From 0ebc9515b4fdebe7c53b619ee71804d1678b3d02 Mon Sep 17 00:00:00 2001 From: Aryan Singh Date: Wed, 2 Sep 2026 11:59:52 -0500 Subject: [PATCH] Try the whole ladder, rather than five eighths of it MAX_ATTEMPTS was 5 while CANDIDATES was 8, so three ranked candidates were gathered and then never tried. The cap was arbitrary and the live numbers show what it cost. slskd answers 500 whenever it cannot open a connection to a peer, and that is common: retrying "Michael Jackson - Billie Jean" walked four unreachable peers before the fifth served it -- succeeding on precisely the last attempt the cap allowed. "The Weeknd - Die For You" ran the same gauntlet and lost, giving up with three untried candidates still on the row. Defaults to CANDIDATES, so the ladder is the ladder. Anyone wanting the old behaviour can still set MUSE_MAX_ATTEMPTS. The cap test lowers the value explicitly now; with the two equal by default it was only ever proving the list ran out, which is the other branch. Co-Authored-By: Claude Opus 5 --- README.md | 2 +- app.py | 9 ++++++++- tests/test_acquisition.py | 8 +++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b794857..98b3d24 100644 --- a/README.md +++ b/README.md @@ -561,7 +561,7 @@ stat -c "%h" "/data/media/music// ()/ - .flac" | `MUSE_MIN_MATCH` | `0.45` | filename-to-track similarity floor | | `MUSE_TITLE_FLOOR` | `0.5` | the title alone must clear this before the artist counts | | `MUSE_CANDIDATES` | `8` | ranked candidates kept per request, for fallback | -| `MUSE_MAX_ATTEMPTS` | `5` | peers tried before a request is given up on | +| `MUSE_MAX_ATTEMPTS` | `MUSE_CANDIDATES` | peers tried before a request is given up on | | `MUSE_ATTEMPT_BACKOFF` | `4` | seconds between candidate attempts | | `MUSE_SYSTEMIC_LIMIT` | `2` | consecutive connection failures before stopping | | `MUSE_STALL_MINUTES` | `20` | a transfer moving no bytes for this long is abandoned | diff --git a/app.py b/app.py index 8b82e4c..eab156a 100644 --- a/app.py +++ b/app.py @@ -80,7 +80,14 @@ TITLE_FLOOR = float(os.getenv("MUSE_TITLE_FLOOR", "0.5")) # one is tried instead of failing the request -- "File not shared." is a # per-file condition, so candidate two usually just works. CANDIDATES = int(os.getenv("MUSE_CANDIDATES", "8")) -MAX_ATTEMPTS = int(os.getenv("MUSE_MAX_ATTEMPTS", "5")) + +# Defaults to the size of the ladder, because keeping eight candidates and then +# refusing to try three of them is a cap with no reasoning behind it. Measured +# on a live instance: slskd answers 500 whenever it cannot open a connection to +# a peer, which is common enough that requests routinely need four or five +# attempts. One succeeded on exactly the fifth; another gave up at the cap with +# three candidates still in hand. +MAX_ATTEMPTS = int(os.getenv("MUSE_MAX_ATTEMPTS", str(CANDIDATES))) # A download that has moved no bytes for this long is not going to. The peer # queued it and vanished, or slskd lost the transfer; either way the job used diff --git a/tests/test_acquisition.py b/tests/test_acquisition.py index 7061840..feebae4 100644 --- a/tests/test_acquisition.py +++ b/tests/test_acquisition.py @@ -209,12 +209,18 @@ def main() -> int: # every peer refuses to accept the transfer fake2.enqueue_status = {f"u{i}": 500 for i in range(8)} app.slskd_client = fake2 + # The cap defaults to the ladder size, so lower it here or this only ever + # proves the list ran out -- which is a different branch. + cap, app.MAX_ATTEMPTS = app.MAX_ATTEMPTS, 3 make_job("j2", "Radiohead", "Creep") asyncio.run(app.pick_and_enqueue("j2", "Radiohead", "Creep")) r = job_row("j2") check("gives up at MAX_ATTEMPTS, not at the end of the list", - r["state"] == "failed" and r["attempts"] == app.MAX_ATTEMPTS, + r["state"] == "failed" and r["attempts"] == 3, f"state={r['state']} attempts={r['attempts']}") + check("and leaves the untried remainder alone", + len(json.loads(r["candidates"] or "[]")) > 0, r["candidates"]) + app.MAX_ATTEMPTS = cap check("unreachable peers are reported as such", "could not reach peer" in (r["tried"] or ""), r["detail"])