fix(worker): cancel abandoned slskd transfers + cap download fall-through
Two compounding causes of 'many downloads of one album from different sources' in slskd: (1) the slskd client raised on timeout/error WITHOUT cancelling the transfer it enqueued, so as the pipeline fell through to other peers each left a transfer running; (2) the incomplete-download fall-through was uncapped, grinding through every candidate of a popular album (~200 Soulseek peers), amplified now that Qobuz pacing pushes most jobs to Soulseek. - SlskdClient.download now cancels its enqueued transfers (DELETE) on any abandon. - The download loop caps at _MAX_DOWNLOAD_ATTEMPTS (6) candidates. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
"""When an slskd download is abandoned (times out or errors), the enqueued transfers must be
|
||||
cancelled — otherwise, as the pipeline falls through to other peers for the same album, abandoned
|
||||
transfers pile up in slskd (many downloads of one album from different sources)."""
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
import lyra_worker.adapters._slskd as slskd_mod
|
||||
from lyra_worker.adapters._slskd import SlskdClient
|
||||
|
||||
|
||||
class _Resp:
|
||||
def __init__(self, payload=None):
|
||||
self._p = payload if payload is not None else {}
|
||||
|
||||
def raise_for_status(self):
|
||||
pass
|
||||
|
||||
def json(self):
|
||||
return self._p
|
||||
|
||||
|
||||
class _FakeRequests:
|
||||
"""Enqueue OK; polling always reports the file still in progress (never completes); records
|
||||
DELETEs so the test can assert the abandoned transfer was cancelled."""
|
||||
def __init__(self, state="InProgress"):
|
||||
self.deleted = []
|
||||
self._poll = {"directories": [{"files": [
|
||||
{"filename": "f1.flac", "size": 5, "state": state, "id": "t1"},
|
||||
]}]}
|
||||
|
||||
def post(self, url, **kw):
|
||||
return _Resp({})
|
||||
|
||||
def get(self, url, **kw):
|
||||
return _Resp(self._poll)
|
||||
|
||||
def delete(self, url, **kw):
|
||||
self.deleted.append(url)
|
||||
return _Resp({})
|
||||
|
||||
|
||||
def _client(tmp_path):
|
||||
return SlskdClient({"slskd.url": "http://x", "slskd.api_key": "k",
|
||||
"slskd.downloadsDir": str(tmp_path)})
|
||||
|
||||
|
||||
_REF = json.dumps({"username": "peerA", "files": [{"filename": "f1.flac", "size": 5}]})
|
||||
|
||||
|
||||
def test_cancels_transfer_on_timeout(tmp_path, monkeypatch):
|
||||
fake = _FakeRequests(state="InProgress")
|
||||
monkeypatch.setattr(slskd_mod, "requests", fake)
|
||||
monkeypatch.setattr(slskd_mod.time, "sleep", lambda _s: None)
|
||||
monkeypatch.setattr(slskd_mod, "_XFER_POLLS", 2)
|
||||
|
||||
with pytest.raises(TimeoutError):
|
||||
_client(tmp_path).download(_REF, str(tmp_path / "dest"), lambda _p: None)
|
||||
|
||||
assert any(u.endswith("/api/v0/transfers/downloads/peerA/t1") for u in fake.deleted), fake.deleted
|
||||
|
||||
|
||||
def test_cancels_transfer_on_error_state(tmp_path, monkeypatch):
|
||||
fake = _FakeRequests(state="Completed, Errored")
|
||||
monkeypatch.setattr(slskd_mod, "requests", fake)
|
||||
monkeypatch.setattr(slskd_mod.time, "sleep", lambda _s: None)
|
||||
monkeypatch.setattr(slskd_mod, "_XFER_POLLS", 5)
|
||||
|
||||
with pytest.raises(RuntimeError):
|
||||
_client(tmp_path).download(_REF, str(tmp_path / "dest"), lambda _p: None)
|
||||
|
||||
assert any(u.endswith("/peerA/t1") for u in fake.deleted), fake.deleted
|
||||
Reference in New Issue
Block a user