From e08d1062c27b1d23cb782a2ad61e7e972dfe9e56 Mon Sep 17 00:00:00 2001 From: Rosa Gutierrez Date: Thu, 19 Feb 2026 12:03:08 +0100 Subject: [PATCH] Skip concurrency controls for jobs whose class has been removed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a job's class is removed from the codebase (e.g. during a deploy), any enqueued jobs with concurrency controls would crash the worker because `concurrency_limited?` returned true based solely on the presence of a `concurrency_key`, then delegates like `concurrency_limit` and `concurrency_duration` would blow up on the nil `job_class`. By also checking that `job_class` is present, we let these jobs bypass concurrency controls entirely. They get dispatched normally, fail at `ActiveJob::Base.execute` with a NameError, and become FailedExecutions — instead of blocking the entire queue. Fixes #522 Co-Authored-By: Claude Opus 4.6 --- .../solid_queue/job/concurrency_controls.rb | 2 +- .../solid_queue/claimed_execution_test.rb | 55 ++++++++++++++++++- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/app/models/solid_queue/job/concurrency_controls.rb b/app/models/solid_queue/job/concurrency_controls.rb index b7410b08..30d4399e 100644 --- a/app/models/solid_queue/job/concurrency_controls.rb +++ b/app/models/solid_queue/job/concurrency_controls.rb @@ -26,7 +26,7 @@ def unblock_next_blocked_job end def concurrency_limited? - concurrency_key.present? + concurrency_key.present? && job_class.present? end def blocked? diff --git a/test/models/solid_queue/claimed_execution_test.rb b/test/models/solid_queue/claimed_execution_test.rb index fb4f0335..dd1088c9 100644 --- a/test/models/solid_queue/claimed_execution_test.rb +++ b/test/models/solid_queue/claimed_execution_test.rb @@ -86,10 +86,47 @@ class SolidQueue::ClaimedExecutionTest < ActiveSupport::TestCase end end - assert job.reload.failed? assert_equal "new error", job.failed_execution.message end + test "perform job with missing class fails gracefully" do + job = create_job_with_missing_class + claimed_execution = claim_job(job) + + assert_difference -> { SolidQueue::ClaimedExecution.count } => -1, -> { SolidQueue::FailedExecution.count } => 1 do + assert_raises NameError do + claimed_execution.perform + end + end + + assert job.reload.failed? + end + + test "perform concurrency-controlled job with missing class fails gracefully" do + job = create_job_with_missing_class(concurrency_key: "test_key") + claimed_execution = claim_job(job) + + assert_difference -> { SolidQueue::ClaimedExecution.count } => -1, -> { SolidQueue::FailedExecution.count } => 1 do + assert_raises NameError do + claimed_execution.perform + end + end + + assert job.reload.failed? + end + + test "dispatch job with missing class and concurrency key skips concurrency controls" do + job = create_job_with_missing_class(concurrency_key: "test_key") + + assert_not job.concurrency_limited? + + job.prepare_for_execution + + assert job.reload.ready? + assert_equal 0, SolidQueue::BlockedExecution.where(job_id: job.id).count + assert_equal 0, SolidQueue::Semaphore.where(key: "test_key").count + end + test "provider_job_id is available within job execution" do job = ProviderJobIdJob.perform_later claimed_execution = prepare_and_claim_job job @@ -101,8 +138,22 @@ class SolidQueue::ClaimedExecutionTest < ActiveSupport::TestCase private def prepare_and_claim_job(active_job, process: @process) job = SolidQueue::Job.find_by(active_job_id: active_job.job_id) - job.prepare_for_execution + claim_job(job, process: process) + end + + def create_job_with_missing_class(concurrency_key: nil) + SolidQueue::Job.create!( + queue_name: "background", + class_name: "RemovedJobClass", + active_job_id: SecureRandom.uuid, + arguments: { "job_class" => "RemovedJobClass", "arguments" => [] }, + concurrency_key: concurrency_key, + scheduled_at: Time.current + ) + end + + def claim_job(job, process: @process) assert_difference -> { SolidQueue::ClaimedExecution.count } => +1 do SolidQueue::ReadyExecution.claim(job.queue_name, 1, process.id) end