Repository navigation
Give concurrency slots back only to the semaphore they came from - #831
Merged
Merged
Conversation
This was referenced Nov 3, 2026
rosa
force-pushed
the
release-concurrency-locks-by-semaphore
branch
from
November 3, 2026 18:20
fcdab10 to
7fce00a
Compare
A semaphore counts free slots but doesn't know which jobs hold them, so any job that gave a slot back by key counted as a holder: - A job that ran past its duration gave its slot back to the semaphore created after its own expired, letting another job start while the new holder was still running (#827). - A job dispatched while its class was missing skipped its limits, but gave a slot back if it later ran where its class loads. - A job whose class was missing when it ran couldn't give back the slot it got when it was dispatched (#825). Now a job records the id of the semaphore it got a slot on, and its limit, in its serialized Active Job data, and gives the slot back only to that semaphore. A job that skips its limits records that it holds no slot. Jobs that got their slot before this change have nothing recorded and keep giving it back by key, until 2.0. The first two tests come from the reproduction in #827. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On MySQL with REPEATABLE READ, a plain read after taking a slot uses the transaction's snapshot, which can still include a semaphore that expired and was deleted after the snapshot was taken, so the job could record that one instead of the semaphore it just created. A locking read sees the latest committed rows. A job that keeps its concurrency key after its class drops limits_concurrency takes its slot with a limit of 1, so record that rather than nil, which failed to give the slot back if the class was also missing when the job ran. Also cover jobs sharing a semaphore, discarding ready jobs one by one and in bulk, and retried jobs, and explain the change in UPGRADING. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t the upgrade note A plain read inside a REPEATABLE READ transaction can still see a semaphore deleted after the transaction's snapshot, so without the locking read the job would record that one. The test fails without it. A slot that isn't given back comes back only when its semaphore expires, and every job taking or giving back a slot for the same key pushes that out, so on a busy key with a limit above 1 it may not come back for a long time. Say so in UPGRADING instead of promising it comes back after duration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rosa
force-pushed
the
release-concurrency-locks-by-semaphore
branch
2 times, most recently
from
November 3, 2026 19:07
5f852d5 to
9dcd3f4
Compare
concurrency_limited? now just means the job has a concurrency key, as on the Active Job side, and the code checks for a missing class where that matters. The job exposes the semaphore it got its slot from and the limit it got it with, so the semaphore asks for those instead of reading the recorded data itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rosa
force-pushed
the
release-concurrency-locks-by-semaphore
branch
from
November 3, 2026 19:26
9dcd3f4 to
0c06081
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #827, and replaces #825.
A concurrency semaphore counts free slots, but it doesn't know which jobs hold them, so any job that gave a slot back by key was treated as a holder. That went wrong in three ways:
duration(A job that outlived its concurrency lock releases the lock of the job that replaced it #827). The dispatcher deletes the expired semaphore and the next job creates a new one. When the first job finishes, it gives its slot back to the new semaphore, so another job starts while the new holder is still running. With a backlog, the key stays one over its limit until the backlog drains.Now, when a job gets a slot, it records the semaphore's id and its concurrency limit in its serialized Active Job data, under
solid_queue_concurrency_lock, which Active Job ignores when running the job. It gives the slot back only to that semaphore. A job that skips its limits records that it holds no slot. A blocked job records its semaphore when it's released.Jobs that got their slot before upgrading have nothing recorded, so they keep giving it back by key, as they do today. Old and new versions can run side by side during a deploy. 2.0 will drop that fallback, so ready and running jobs from before the upgrade should finish first: a slot that isn't given back only returns when its semaphore expires, and on a busy key with a limit above 1 the other jobs keep pushing that expiry out.
One case this doesn't cover, and that we accept: during a deploy, if a failed job is retried by a process still on the old version and then run by one on the new version, the old process doesn't update the recorded semaphore, so the new one gives the slot back to the wrong place. With a limit of 1, the slot comes back when the semaphore expires; with a higher limit on a busy key, the key can run one slot short for as long as it stays busy. It needs all of that to happen inside the deploy window, and it never lets jobs run over the limit.
The change is meant to ship on its own in 1.7.2, with a note in UPGRADING.
The first two tests come from the reproduction in #827 by @npadgett.
🤖 Generated with Claude Code