Nobody has started this yet — be first.
Business impact
WORKER_CONCURRENCY is the operator's only lever for controlling how much simultaneous load a single instance throws at downstream targets and how many goroutines/HTTP connections it opens at once. If it silently stops working, a burst of simultaneously-due work spawns one goroutine and one outbound HTTP request per due run, all at once, regardless of the configured limit. On a modest instance this can exhaust file descriptors, saturate the network, or hammer a downstream target hard enough to trip its rate limiting or take it down -- the exact failure mode WORKER_CONCURRENCY exists to prevent, happening silently because the config value is being read and stored correctly, just never enforced.
Problem
pkg/worker/pool.go's Pool.Run is supposed to use a buffered channel (slots) as a semaphore capping concurrent executions at cfg.Concurrency. The pattern only works if the slot is released when the goroutine finishes (inside the goroutine, after runOne returns). Here, <-slots runs immediately after launching the goroutine, in the outer loop, not after the execution completes -- a slot is freed the instant a run starts, not when it finishes, so slots <- struct{}{} essentially never blocks.
Current behavior
With Concurrency: 2 configured, feeding the pool 6 simultaneously-due runs against a slow target shows the target seeing up to 6 concurrent in-flight requests instead of at most 2.
Expected behavior
No more than cfg.Concurrency executions should ever be in flight at the same time, regardless of how many runs are leased in a single poll.
Steps to reproduce
Why this matters
This is a textbook "moved code to the wrong scope during a refactor" bug: the semaphore acquire (slots <- struct{}{}) and release (<-slots) must be on either side of the work the semaphore is meant to bound. Here, the release moved from "after the protected work finishes" to "immediately after starting it," which silently turns a bounded-concurrency pattern into an unbounded one while looking, at a glance, like the same pattern.
Suggested approach
Compare where slots <- struct{}{} (acquire) happens relative to where <-slots (release) happens, and specifically when the release actually runs relative to p.runOne completing. The defer p.wg.Done() line right next to it is a hint about where a similar "run this when the goroutine is actually finished" mechanism already exists in this function.
Acceptance criteria
Verification
TEST_DATABASE_URL="postgresql://chronos:chronos@localhost:5432/chronos?sslmode=disable" go test -race ./tickets/tests/... -run TestTicket17 -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 fix/worker-concurrency-slotFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/worker-concurrency-slotSubmit 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 inhttp://localhost:8081app-1http://localhost:8082app-2http://localhost:8090curl localhost:8081/healthzcurl localhost:8081/readyzWork the tickets in tickets/docs/ (ordered easy → hard); each names one Go test in tickets/tests/. Most tickets need a real Postgres (tickets 4, 11, and 18 are pure-function tests and do not):
export TEST_DATABASE_URL="postgresql://chronos:chronos@localhost:5432/chronos?sslmode=disable"
go test ./tickets/tests/... -run TestTicket08 -v # a single ticket
./tickets/run_tickets.sh # all 20, clean pass/fail summary
Use -race when working on tickets 15, 16, or 17 (the concurrency ones) -- run_tickets.sh already does this for every ticket. Tickets 15 and 16 are adjacent bugs in the same function (pkg/leader/leader.go's lock-release path); fix ticket 16 first, since ticket 15's test keeps failing for ticket 16's reason until that one is fixed too.
Level 1
Fix a bug
Read existing behaviour, correct it.