Retrying jobs in Rails 8
- Date
- 13 March, 2026
- Category
- code
- Tags
- Ruby on Rails
I am working on a project right now where we wait until we confirm a customer's payment has succeeded, then send a request to a 3rd party to start the corresponding service up. We're using Rails' ActiveJob with SolidQueue to manage this request.
A coworker had already written a job to activate the 3rd party service several times and logged the failures. This was my starting point:
class ActivateRecordJob < ApplicationJob
queue_as :default
class ActivationError < StandardError; end
retry_on ActivationError, wait: :polynomially_longer, attempts: 5
discard_on ActiveJob::DeserializationError
def perform(record)
if ActivateInteractor.call(record)
record.update(status: :published)
else
Rails.logger.error("Activation failed for record #{record.id}, attempt #{executions}")
raise ActivationError
end
end
end
When the job is picked up off the queue, the following happens:
- It calls the activation interactor which returns true / false for success / failure
- If it succeeds, the job is done, hooray
- If it fails, it raises
ActivationErrorwhich then prompts it to retry. It will retry 4 times before giving up
after_discard
If the activation failed, I wanted to add some code that would refund the customer, since the thing they paid for was unavailable.
It took me a little bit to figure out because I'm not terribly familiar with ActiveJob. Most of the examples use the discard_on hook in concert with the after_discard hook and I wasn't sure if that was required to consider a job "discarded." However, after a little trial and error, I discovered that if the retries fail enough times, the job is considered discarded. If the job passes, it is NOT discarded. That's all that I needed!
I had to make my record variable in the example above an instance variable so that the value would be available in the after_discard hook, which then looked like this:
after_discard do |job, exception|
@record.update(status: :canceled)
CancelInteractor.call(@record)
end
Testing
As always, testing remains my Achilles heel. Fortunately, it ended up not being too difficult to figure out how to write unit tests that would confirm that the job was ultimately running the code in after_discard on failure.
Success
describe "when activation succeeds" do
it "publishes the record" do
ThirdParty::Service.stub(:activate, true, [record.service_id]) do
ActivateRecordJob.perform_now(record)
end
record.reload
assert record.published?
end
end
Failure
describe "when activation fails repeatedly" do
it "cancels the record" do
ActivateInteractor.stub(:call, false) do
CancelInteractor.stub(:call, true) do
perform_enqueued_jobs do
assert_raises ActivateRecordJob::ActivationError do
ActivateRecordJob.perform_later(record)
end
end
end
end
record.reload
assert record.canceled?
end
end