Skip to content

Commit 73602fe

Browse files
rosaclaude
andcommitted
Unify the error for jobs whose class no longer resolves
Both a worker picking up a job whose class is gone and the concurrency maintenance releasing a blocked one now fail the job with SolidQueue::Job::ClassMissingError. Before, the first case surfaced as whatever NameError Active Job's deserialization raised, and the second one, just added, had its own BlockedExecution::JobClassMissingError, so the same user mistake read differently in failed jobs depending on where it was caught. The error subclasses NameError, which is what resolving the class raises, so anything matching on NameError still matches. Also surface the new failed: key in the release_blocked log line, document the behaviour in the README's concurrency controls section, and add the magic comment to the new test file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CACej2M9mLVDwpE3Bk8V41
1 parent 4e8fe60 commit 73602fe

7 files changed

Lines changed: 21 additions & 11 deletions

File tree

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,8 @@ It's important to note that after one or more candidate jobs are unblocked (eith
555555

556556
When using `discard` as the behaviour to handle conflicts, you might have jobs discarded for until the `duration` interval if something happens and a running job fails to release the semaphore.
557557

558+
If a job's class no longer exists by the time its concurrency controls are checked—say it was renamed or removed in a deploy while jobs referencing it were still in the queue—the job is marked as failed with a `SolidQueue::Job::ClassMissingError`, so it shows up in [failed jobs](#failed-jobs-and-retries), where it can be retried once the class is back, or discarded. Jobs with a missing class picked up by a worker fail with the same error.
559+
558560

559561
For example:
560562
```ruby

‎app/models/solid_queue/blocked_execution.rb‎

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
module SolidQueue
44
class BlockedExecution < Execution
5-
class JobClassMissingError < RuntimeError; end
6-
75
assumes_attributes_from_job :concurrency_key
86
before_create :set_expires_at
97

@@ -49,12 +47,9 @@ def release
4947
SolidQueue.instrument(:release_blocked, job_id: job.id, concurrency_key: concurrency_key, released: false) do |payload|
5048
transaction do
5149
if job.job_class.nil?
52-
# The job's class no longer resolves (renamed/removed between deploys).
53-
# Mark the job as failed so it surfaces in Mission Control (where it can
54-
# be retried once the class is restored, or discarded), and destroy the
55-
# orphan row so the dispatcher stops retrying it forever.
56-
job.failed_with(JobClassMissingError.new("Job class #{job.class_name.inspect} could not be resolved"))
50+
job.failed_with(Job::ClassMissingError.for(job))
5751
destroy!
52+
5853
payload[:failed] = true
5954
elsif acquire_concurrency_lock
6055
promote_to_ready

‎app/models/solid_queue/claimed_execution.rb‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ def finalizing
115115
end
116116

117117
def execute
118+
raise Job::ClassMissingError.for(job) if job.job_class.nil?
119+
118120
ActiveJob::Base.execute(job.arguments.merge("provider_job_id" => job.id))
119121
Result.new(true, nil)
120122
rescue Exception => e

‎app/models/solid_queue/job.rb‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ module SolidQueue
44
class Job < Record
55
class EnqueueError < StandardError; end
66

7+
# Raised when a job's class can't be resolved anymore, typically because it
8+
# was renamed or removed in a deploy while jobs referencing it were in
9+
# flight. It subclasses NameError, which is what resolving the class raises.
10+
class ClassMissingError < NameError
11+
def self.for(job)
12+
new("Job class #{job.class_name.inspect} could not be resolved")
13+
end
14+
end
15+
716
include Executable, Clearable, Recurrable, Batchable
817

918
serialize :arguments, coder: JSON

‎lib/solid_queue/log_subscriber.rb‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def release_many_blocked(event)
5959
end
6060

6161
def release_blocked(event)
62-
debug formatted_event(event, action: "Release blocked job", **event.payload.slice(:job_id, :concurrency_key, :released))
62+
debug formatted_event(event, action: "Release blocked job", **event.payload.slice(:job_id, :concurrency_key, :released, :failed))
6363
end
6464

6565
def enqueue_recurring_task(event)

‎test/models/solid_queue/blocked_execution_test.rb‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# frozen_string_literal: true
2+
13
require "test_helper"
24

35
class SolidQueue::BlockedExecutionTest < ActiveSupport::TestCase
@@ -44,7 +46,7 @@ def perform(job_result)
4446

4547
failed = SolidQueue::FailedExecution.find_by(job_id: blocked_job.id)
4648
assert failed, "expected a failed execution to be created for the orphan"
47-
assert_equal "SolidQueue::BlockedExecution::JobClassMissingError", failed.exception_class
49+
assert_equal "SolidQueue::Job::ClassMissingError", failed.exception_class
4850
assert_match "GoneJob", failed.message
4951
end
5052
end

‎test/models/solid_queue/claimed_execution_test.rb‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ class SolidQueue::ClaimedExecutionTest < ActiveSupport::TestCase
146146
claimed_execution = claim_job(job)
147147

148148
assert_difference -> { SolidQueue::ClaimedExecution.count } => -1, -> { SolidQueue::FailedExecution.count } => 1 do
149-
assert_raises NameError do
149+
assert_raises SolidQueue::Job::ClassMissingError do
150150
claimed_execution.perform
151151
end
152152
end
@@ -159,7 +159,7 @@ class SolidQueue::ClaimedExecutionTest < ActiveSupport::TestCase
159159
claimed_execution = claim_job(job)
160160

161161
assert_difference -> { SolidQueue::ClaimedExecution.count } => -1, -> { SolidQueue::FailedExecution.count } => 1 do
162-
assert_raises NameError do
162+
assert_raises SolidQueue::Job::ClassMissingError do
163163
claimed_execution.perform
164164
end
165165
end

0 commit comments

Comments
 (0)