fix(downloads): worker claim fairness + bounded job listings (R12 S5)

#2 — _claim_job excluded users at their concurrency limit only in Python,
AFTER a fixed LIMIT-50 fetch window. One user flooding the queue with >50
queued jobs while at their cap filled the whole window, chosen came back None,
and every other user's work past position 50 was never seen. Exclude capped
users IN THE SQL (per-user max_concurrent from the quota row, else the
sysconfig default; unlimited users never capped), keeping the Python re-check
as a race guard that can only reject, never over-claim.

#3 — list_downloads and shared_with_me loaded every job for a user with no
bound. Cap both at 500 newest-first (a shared _MAX_LIST, matching the admin
listing's existing cap); the Download Center renders newest-first and 500 is
far beyond any real manual-download history, so no frontend change is needed.

Tests: DB-backed — a capped user's 60-job flood no longer starves a user
behind the window; oldest-by-queue_pos claim, all-capped->None, unlimited
never excluded; and the list cap bounds the result set.
This commit is contained in:
2026-08-13 02:02:20 +02:00
parent 222edaf99e
commit 2a33fc0d42
3 changed files with 137 additions and 4 deletions
+8 -1
View File
@@ -40,6 +40,11 @@ admin_router = APIRouter(prefix="/api/admin/downloads", tags=["admin-downloads"]
_VIDEO_ID = re.compile(r"^[A-Za-z0-9_-]{11}$")
# Cap the per-user job listings so a heavy library can't return an unbounded result set. The
# Download Center renders newest-first; 500 is far beyond any real manual-download history (and
# matches the admin listing's own cap).
_MAX_LIST = 500
# --- source + serialization ----------------------------------------------------------------
@@ -334,6 +339,7 @@ def list_downloads(
select(DownloadJob)
.where(DownloadJob.user_id == user.id)
.order_by(DownloadJob.created_at.desc(), DownloadJob.id.desc())
.limit(_MAX_LIST)
)
.scalars()
.all()
@@ -380,6 +386,7 @@ def shared_with_me(
.join(DownloadShare, DownloadShare.job_id == DownloadJob.id)
.where(DownloadShare.to_user_id == user.id)
.order_by(DownloadShare.created_at.desc())
.limit(_MAX_LIST)
)
.scalars()
.all()
@@ -881,7 +888,7 @@ def admin_list_downloads(
) -> list[dict]:
jobs = (
db.execute(
select(DownloadJob).order_by(DownloadJob.created_at.desc(), DownloadJob.id.desc()).limit(500)
select(DownloadJob).order_by(DownloadJob.created_at.desc(), DownloadJob.id.desc()).limit(_MAX_LIST)
)
.scalars()
.all()