Files
shelfmark/tests/download/test_client_queued.py
T
splitsec2 06a67a7150 feat(download): a concurrency limit per source (#1495)
With one global limit, a few tasks that are only waiting hold every
slot. A Soulseek download sitting in a peer's queue is the example I
hit: three of those and a torrent that could start waits behind them.

`MAX_CONCURRENT_DOWNLOADS_PER_SOURCE` (0 keeps the single global limit,
which is the default). With it set, the coordinator starts a download
only when its source is below the limit and passes over tasks of a full
source without losing their place in the queue. `get_next` gets an
optional filter for this, and a cancelled task is still dropped when one
is given. The start-up stagger now applies only between downloads from
the same source, and the pool is sized so every source can reach its
limit.

The second commit: qBittorrent keeps its own queue of torrents waiting
for a free slot. The task polling one of those does no work, so it no
longer counts against its source's limit and the next task of that
source can start. The client does the pacing, as it does for its own
queue.

Tests cover the queue pass-over (order and priority kept, nothing
eligible, cancelled tasks), the coordinator (a full source doesn't block
the others, each source has its own limit, the global limit when unset)
and the client-queue case. Ruff and the backend suite pass.
2026-10-20 23:19:33 -04:00

52 lines
1.8 KiB
Python

"""A torrent queued inside its client is not working yet, and the task says so."""
from __future__ import annotations
from threading import Event
from unittest.mock import MagicMock, patch
from shelfmark.core.models import DownloadTask
from shelfmark.download.clients import DownloadState, DownloadStatus
from shelfmark.release_sources.prowlarr.handler import ProwlarrHandler
def _status(state: DownloadState) -> DownloadStatus:
return DownloadStatus(progress=0.0, state=state, message=None, complete=False, file_path=None)
def _poll(states: list[DownloadState]) -> tuple[DownloadTask, list[bool]]:
cancel = Event()
seen = iter(states)
flags: list[bool] = []
task = DownloadTask(task_id="t", source="prowlarr", title="Book")
client = MagicMock()
client.name = "qbittorrent"
def get_status(_id: str) -> DownloadStatus:
flags.append(task.client_queued) # what the previous poll left behind
try:
return _status(next(seen))
except StopIteration:
cancel.set()
return _status(states[-1])
client.get_status.side_effect = get_status
with patch.object(ProwlarrHandler, "_poll_interval", return_value=0.0):
ProwlarrHandler()._poll_and_complete(
client, "hash", "torrent", task, cancel, lambda _p: None, lambda *_a: None
)
return task, flags
def test_the_task_is_marked_while_its_torrent_waits_in_the_clients_queue():
queued, downloading = DownloadState.QUEUED, DownloadState.DOWNLOADING
_task, flags = _poll([queued, queued, downloading, downloading])
assert flags[:5] == [False, True, True, False, False]
def test_a_task_that_never_waited_is_never_marked():
_task, flags = _poll([DownloadState.DOWNLOADING] * 3)
assert not any(flags)