Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 395944f3e1 |
@@ -12,9 +12,25 @@ _DEFAULT_DOWNLOADS_ROOT = "/slskd-downloads"
|
|||||||
_AUDIO_EXT = {"flac", "mp3", "m4a", "ogg", "opus", "wav", "aac"}
|
_AUDIO_EXT = {"flac", "mp3", "m4a", "ogg", "opus", "wav", "aac"}
|
||||||
_LOSSLESS_EXT = {"flac", "wav"}
|
_LOSSLESS_EXT = {"flac", "wav"}
|
||||||
_SEARCH_POLLS = 40 # * 3s ≈ 2 min
|
_SEARCH_POLLS = 40 # * 3s ≈ 2 min
|
||||||
_MAX_XFER_POLLS = 1200 # * 3s ≈ 60 min absolute backstop for a slow-but-progressing peer
|
|
||||||
_STALL_POLLS = 30 # * 3s ≈ 90s of no byte progress → abandon a queued/dead peer and fall through
|
_STALL_POLLS = 30 # * 3s ≈ 90s of no byte progress → abandon a queued/dead peer and fall through
|
||||||
_POLL_SECONDS = 3
|
_POLL_SECONDS = 3
|
||||||
|
# Absolute backstop for a slow-but-progressing peer. Scaled to the album's total bytes (a flat
|
||||||
|
# cap starved big albums: a 500MB set from a ~400KB/s serial peer needs ~60-70min and was cut at
|
||||||
|
# a flat 60min, then re-downloaded from scratch — never finishing). Budget at a conservative floor
|
||||||
|
# throughput, clamped. The 90s stall timeout still abandons a truly dead/queued peer quickly.
|
||||||
|
_MIN_XFER_POLLS = 400 # * 3s ≈ 20 min floor (small albums)
|
||||||
|
_MAX_XFER_POLLS = 2400 # * 3s ≈ 2 h ceiling (bounds worst-case slot hold)
|
||||||
|
_BACKSTOP_FLOOR_BPS = 64_000 # budget the backstop assuming >= ~64 KB/s sustained
|
||||||
|
|
||||||
|
|
||||||
|
def _max_polls(total_bytes: int) -> int:
|
||||||
|
"""Poll budget (each _POLL_SECONDS) for the absolute download backstop, scaled to album size at
|
||||||
|
a conservative floor throughput so a large slow-but-healthy transfer isn't abandoned mid-flight.
|
||||||
|
Clamped to [_MIN_XFER_POLLS, _MAX_XFER_POLLS]. Pure — unit-tested."""
|
||||||
|
if total_bytes <= 0:
|
||||||
|
return _MIN_XFER_POLLS
|
||||||
|
budget = int(total_bytes / (_BACKSTOP_FLOOR_BPS * _POLL_SECONDS))
|
||||||
|
return max(_MIN_XFER_POLLS, min(budget, _MAX_XFER_POLLS))
|
||||||
|
|
||||||
|
|
||||||
def _basename(path: str) -> str:
|
def _basename(path: str) -> str:
|
||||||
@@ -242,7 +258,7 @@ class SlskdClient:
|
|||||||
speed = 0.0 # bytes/sec, EWMA-smoothed so the ETA doesn't jitter poll to poll
|
speed = 0.0 # bytes/sec, EWMA-smoothed so the ETA doesn't jitter poll to poll
|
||||||
prev_bytes = 0
|
prev_bytes = 0
|
||||||
prev_t = time.monotonic()
|
prev_t = time.monotonic()
|
||||||
for _ in range(_MAX_XFER_POLLS):
|
for _ in range(_max_polls(total_bytes)):
|
||||||
time.sleep(_POLL_SECONDS)
|
time.sleep(_POLL_SECONDS)
|
||||||
data = self._get(f"/api/v0/transfers/downloads/{username}")
|
data = self._get(f"/api/v0/transfers/downloads/{username}")
|
||||||
states: list[str] = []
|
states: list[str] = []
|
||||||
|
|||||||
@@ -8,8 +8,12 @@ import pytest
|
|||||||
import lyra_worker.adapters._slskd as slskd_mod
|
import lyra_worker.adapters._slskd as slskd_mod
|
||||||
from lyra_worker.adapters._slskd import (
|
from lyra_worker.adapters._slskd import (
|
||||||
_ETA_CAP_SECONDS,
|
_ETA_CAP_SECONDS,
|
||||||
|
_MAX_XFER_POLLS,
|
||||||
|
_MIN_XFER_POLLS,
|
||||||
|
_POLL_SECONDS,
|
||||||
SlskdClient,
|
SlskdClient,
|
||||||
_eta_seconds,
|
_eta_seconds,
|
||||||
|
_max_polls,
|
||||||
_parse_search_responses,
|
_parse_search_responses,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -162,6 +166,25 @@ def test_eta_seconds_unknown_cases():
|
|||||||
assert _eta_seconds(1000, 1000, 100.0) is None # already complete
|
assert _eta_seconds(1000, 1000, 100.0) is None # already complete
|
||||||
|
|
||||||
|
|
||||||
|
def test_max_polls_scales_backstop_to_album_size():
|
||||||
|
# A 521MB album (Drake "Scorpion", 25 FLACs) from a slow serial peer must get far more than
|
||||||
|
# the old flat 1200-poll/60min budget, or it gets abandoned mid-download and restarts from zero.
|
||||||
|
scorpion_bytes = 521_000_000
|
||||||
|
polls = _max_polls(scorpion_bytes)
|
||||||
|
assert polls > 1200 # more headroom than the old flat cap
|
||||||
|
assert polls * _POLL_SECONDS >= 90 * 60 # at least ~90 min of wall-clock budget
|
||||||
|
|
||||||
|
# A tiny single stays on the floor, not zero.
|
||||||
|
assert _max_polls(3_000_000) == _MIN_XFER_POLLS
|
||||||
|
assert _max_polls(0) == _MIN_XFER_POLLS
|
||||||
|
|
||||||
|
# An enormous set is clamped to the ceiling (bounds worst-case slot hold).
|
||||||
|
assert _max_polls(50_000_000_000) == _MAX_XFER_POLLS
|
||||||
|
|
||||||
|
# Monotonic: bigger album never gets a smaller budget.
|
||||||
|
assert _max_polls(200_000_000) <= _max_polls(400_000_000)
|
||||||
|
|
||||||
|
|
||||||
def test_eta_seconds_capped_when_peer_nearly_stalls():
|
def test_eta_seconds_capped_when_peer_nearly_stalls():
|
||||||
# A near-stalled peer (tiny speed) would yield an ETA far past int4, overflowing the
|
# A near-stalled peer (tiny speed) would yield an ETA far past int4, overflowing the
|
||||||
# downloadEtaSeconds column write. The estimate must be clamped to a safe ceiling.
|
# downloadEtaSeconds column write. The estimate must be clamped to a safe ceiling.
|
||||||
|
|||||||
Reference in New Issue
Block a user