Skip to content

Add coalesce to deduplicate pending jobs - #835

Open
dtcristo wants to merge 3 commits into
rails:mainfrom
dtcristo:xenorune/coalesce-jobs
Open

dtcristo wants to merge 3 commits into
rails:mainfrom
dtcristo:xenorune/coalesce-jobs

Conversation

@dtcristo

@dtcristo dtcristo commented Nov 7, 2026 •

Copy link
Copy Markdown

Hey, David here.

Below is my AI-assisted implementation of basic coalescing described in #834.


What

Adds coalesce key: to Active Job classes. Jobs enqueued with the same key share one pending job. The first creates it, and later ones, including scheduled ones, are discarded into it until a worker claims it. A job enqueued during the run then queues the next pending job. coalesce_status (:created or :discarded) and provider_job_id tell the caller what happened.

It adds a solid_queue_coalesce_keys table, with bin/rails solid_queue:update for existing installations.

Why

See #834, which is the place to discuss the design. This PR is a minimal version to make that discussion concrete. It shows the approach, and leaves out edge cases a real implementation would need.

How

  • ActiveJob::Coalescing sits next to ActiveJob::ConcurrencyControls. It adds coalesce and builds keys the way concurrency_key does, scoped to the class and batch.
  • Job.enqueue sends coalesced jobs to SolidQueue::Job::Coalescing, the way Job::ConcurrencyControls pairs with its Active Job counterpart. In one transaction, it inserts the key's row unless it exists, locks it, and either creates the job and records it on the row, or discards the call into the job already there. Calls with the same key wait on that lock. Database errors raise EnqueueError, as for other jobs. Job.enqueue_all handles coalesced jobs one at a time, in key order.
  • ReadyExecution.claim locks the keys of the jobs it claims in key order, the same order as bulk enqueue, and deletes them in the claim's transaction. A pending job that's deleted takes its key with it, through the foreign key.
  • Retries aren't coalesced, because they continue work that already started.

Left out on purpose: deadlock retries on MySQL, instrumentation, and keeping the key for a claimed job that's released back to the queue. An app transaction that coalesces several keys in arbitrary order can also deadlock with a claim.

🤖 Written by @dtcristo's agent (Claude Opus 5.5, GPT-6.1 Sol)

@dtcristo
dtcristo force-pushed the xenorune/coalesce-jobs branch from 2627df4 to a856eb3 Compare November 7, 2026 05:40
@dtcristo dtcristo changed the title Coalescing - Keep one pending job per key [DRAFT] Coalesce - Basic discard of duplicate jobs at enqueue time Nov 7, 2026
@dtcristo dtcristo changed the title [DRAFT] Coalesce - Basic discard of duplicate jobs at enqueue time Add coalesce to deduplicate pending jobs Nov 7, 2026
@xenorune
xenorune force-pushed the xenorune/coalesce-jobs branch 2 times, most recently from 2138fef to 918ca08 Compare November 7, 2026 06:57
A row ties a coalesce key to the pending job that calls with that key are
discarded into. Fresh installs get it with the base schema, and existing ones
through `bin/rails solid_queue:update`. Its foreign key deletes the row along
with the job.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@xenorune
xenorune force-pushed the xenorune/coalesce-jobs branch 3 times, most recently from fb3af4f to d02d173 Compare November 7, 2026 07:51
dtcristo and others added 2 commits November 7, 2026 18:56
`coalesce key:` makes jobs with the same key share one pending job. Admission
inserts the key's row unless it exists and locks it, then creates the job or
discards the call into the one already there. Claiming a job releases its key,
so a call made during the run queues the next job. Retries aren't coalesced.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@xenorune
xenorune force-pushed the xenorune/coalesce-jobs branch from d02d173 to d42f86b Compare November 7, 2026 07:56
@dtcristo
dtcristo marked this pull request as ready for review November 7, 2026 08:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant