Nobody has started this yet — be first.
Business impact
POST /jobs unconditionally spawns a worker task for every request, regardless of how many jobs are already running. In this simulated service that just means more concurrent asyncio.sleep() calls, but this project's own README frames the simulated "transcode" as a stand-in for a real, resource-hungry job (a real transcoder's CPU/GPU budget). A real backing implementation would have a hard concurrency ceiling somewhere -- worker processes, GPU slots, external API rate limits -- and nothing in this service protects it. A traffic spike, a buggy client retry loop, or just organic growth in usage would all hit that ceiling by simply overwhelming it, with this service doing nothing to prevent, queue, or even report on it happening.
Problem
Settings.max_concurrent_jobs already exists as a config knob for exactly this, but nothing reads it.
Current behavior
create_job() unconditionally calls asyncio.create_task(run_job(...)) for every request regardless of how many jobs are already running, and nothing reads Settings.max_concurrent_jobs anywhere.
Expected behavior
At most max_concurrent_jobs jobs are ever actively running (status="running") at once. A POST /jobs call submitted while at capacity still succeeds (202, job created in status="queued"), but its worker doesn't actually start doing work until a slot frees up. max_concurrent_jobs=0 means unlimited (today's actual, current behavior).
Steps to reproduce
pytest practice_tickets/tests/test_ticket_15_no_concurrency_cap.py -vmax_concurrent_jobs=1, two jobs submitted back to back both start immediately -- jobs_in_progress rises by 2.0 instead of the expected +1.Why this matters
This is a genuine concurrency-control problem, not just "add a condition before spawning the task" -- the desired behavior is closer to a bounded work queue: accept the job immediately (the caller gets a real job_id back right away and can start polling it), but don't let its actual work begin until capacity exists. Whatever gates this has to be shared across every job's worker, held for the job's entire active lifetime, and released reliably even when a job fails or is cancelled midway through -- get that release wrong and every job after the first max_concurrent_jobs failures/cancellations queues forever with no possible way to make progress.
Suggested approach
app/main.py's /lifespan is the natural place to own a single shared across every job (mirroring how it already owns ) -- needs a decision here too, since a semaphore can't take an unlimited value. 's is where a slot would need to be acquired before the job's real work begins and released once it ends, no matter which of the three ways it ends -- the block already established for / is the right place to think about "always runs, no matter how the job ends."
Acceptance criteria
Verification
pytest practice_tickets/tests/test_ticket_15_no_concurrency_cap.py -v
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
Sign in to create it automatically, or do it yourself:
$git checkout -b feat/no-concurrency-capFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin feat/no-concurrency-capSubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign increate_app()asyncio.Semaphore(settings.max_concurrent_jobs)app.state.registrymax_concurrent_jobs=0app/worker.pyrun_job()finallyjobs_in_progressregistry.discard()http://localhost:8000localhost:6379curl localhost:8000/healthz{"status": "ok", "redis": true}Work the tickets in practice_tickets/tickets/ (ordered easy → hard); each names one dedicated pytest file in practice_tickets/tests/:
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements-dev.txt
pytest practice_tickets/tests/test_ticket_01_completed_jobs_counted_as_failed.py -v # a single ticket
./practice_tickets/run_tickets.sh # all 15, clean pass/fail summary
Each ticket test runs in its own pytest process with a 60s timeout, so one hung/crashed ticket can't take down the whole run or hide another ticket's result. Run pytest tests/ (the project's own suite, against fakeredis, hermetic and fast) after each fix to check for regressions -- the README documents exactly which fixes are expected to change that suite's results.
Level 2
Implement a feature
Extend the system within its own patterns.