This method lets job authors define a block which will be run when a job is about to be discarded.

This has utility for gems/modules included on jobs, which can tie into this behaviour and run something when a job fails.
after_discard respects the existing retry behaviour, but will run even if a retried exception is handled in a block.
This commit is contained in:
Rob Cardy 2023-05-01 15:50:31 -04:00
parent e49fcfa0dc
commit 659d41110f
No known key found for this signature in database
GPG Key ID: C6843FD1B87E1992
5 changed files with 127 additions and 1 deletions

@ -1,3 +1,24 @@
* Add `after_discard` method
This method lets job authors define a block which will be run when a job is about to be discarded. For example:
```ruby
class AfterDiscardJob < ActiveJob::Base
after_discard do |job, exception|
Rails.logger.info("#{job.class} raised an exception: #{exception}")
end
def perform
raise StandardError
end
end
```
The above job will run the block passed to `after_discard` after the job is discarded. The exception will
still be raised after the block has been run.
*Rob Cardy*
* Allow queue adapters to provide a custom name by implementing `queue_adapter_name`
*Sander Verdonschot*

@ -9,6 +9,7 @@ module Exceptions
included do
class_attribute :retry_jitter, instance_accessor: false, instance_predicate: false, default: 0.0
class_attribute :after_discard_procs, default: []
end
module ClassMethods
@ -65,8 +66,10 @@ def retry_on(*exceptions, wait: 3.seconds, attempts: 5, queue: nil, priority: ni
instrument :retry_stopped, error: error do
yield self, error
end
run_after_discard_procs(error)
else
instrument :retry_stopped, error: error
run_after_discard_procs(error)
raise error
end
end
@ -95,9 +98,26 @@ def discard_on(*exceptions)
rescue_from(*exceptions) do |error|
instrument :discard, error: error do
yield self, error if block_given?
run_after_discard_procs(error)
end
end
end
# A block to run when a job is about to be discarded for any reason.
#
# ==== Example
#
# class WorkJob < ActiveJob::Base
# after_discard do |job, exception|
# ExceptionNotifier.report(exception)
# end
#
# ...
#
# end
def after_discard(&blk)
self.after_discard_procs += [blk]
end
end
# Reschedules the job to be re-executed. This is useful in combination
@ -164,5 +184,15 @@ def executions_for(exceptions)
executions
end
end
def run_after_discard_procs(exception)
exceptions = []
after_discard_procs.each do |blk|
instance_exec(self, exception, &blk)
rescue StandardError => e
exceptions << e
end
raise exceptions.last unless exceptions.empty?
end
end
end

@ -45,7 +45,11 @@ def perform_now
_perform_job
rescue Exception => exception
rescue_with_handler(exception) || raise
handled = rescue_with_handler(exception)
return handled if handled
run_after_discard_procs(exception)
raise
end
def perform(*)

@ -2,6 +2,7 @@
require "helper"
require "jobs/retry_job"
require "jobs/after_discard_retry_job"
require "models/person"
require "minitest/mock"
@ -332,6 +333,43 @@ class ExceptionsTest < ActiveSupport::TestCase
assert_equal ["Raised DefaultsError for the 5th time"], JobBuffer.values
end
test "#after_discard block is run when an unhandled error is raised" do
assert_raises(AfterDiscardRetryJob::UnhandledError) do
AfterDiscardRetryJob.perform_later("AfterDiscardRetryJob::UnhandledError", 2)
end
assert_equal "Ran after_discard for job. Message: AfterDiscardRetryJob::UnhandledError", JobBuffer.last_value
end
test "#after_discard block is run when #retry_on is passed a block" do
AfterDiscardRetryJob.perform_later("AfterDiscardRetryJob::CustomCatchError", 6)
assert_equal "Ran after_discard for job. Message: AfterDiscardRetryJob::CustomCatchError", JobBuffer.last_value
end
test "#after_discard block is only run once when an error class and its superclass are handled by separate #retry_on calls" do
assert_raises(AfterDiscardRetryJob::ChildAfterDiscardError) do
AfterDiscardRetryJob.perform_later("AfterDiscardRetryJob::ChildAfterDiscardError", 6)
end
assert_equal ["Raised AfterDiscardRetryJob::ChildAfterDiscardError for the 5th time", "Ran after_discard for job. Message: AfterDiscardRetryJob::ChildAfterDiscardError"], JobBuffer.values.last(2)
end
test "#after_discard is run when a job is discarded via #discard_on" do
AfterDiscardRetryJob.perform_later("AfterDiscardRetryJob::DiscardableError", 2)
assert_equal "Ran after_discard for job. Message: AfterDiscardRetryJob::DiscardableError", JobBuffer.last_value
end
test "#after_discard is run when a job is discarded via #discard_on with a block passed to #discard_on" do
AfterDiscardRetryJob.perform_later("AfterDiscardRetryJob::CustomDiscardableError", 2)
expected_array = [
"Dealt with a job that was discarded in a custom way. Message: AfterDiscardRetryJob::CustomDiscardableError",
"Ran after_discard for job. Message: AfterDiscardRetryJob::CustomDiscardableError"
]
assert_equal expected_array, JobBuffer.values.last(2)
end
private
def adapter_skips_scheduling?(queue_adapter)
[

@ -0,0 +1,33 @@
# frozen_string_literal: true
require_relative "../support/job_buffer"
require "active_support/core_ext/integer/inflections"
class AfterDiscardRetryJob < ActiveJob::Base
class UnhandledError < StandardError; end
class DefaultsError < StandardError; end
class CustomCatchError < StandardError; end
class DiscardableError < StandardError; end
class CustomDiscardableError < StandardError; end
class AfterDiscardError < StandardError; end
class ChildAfterDiscardError < AfterDiscardError; end
retry_on DefaultsError
retry_on(CustomCatchError) { |job, error| JobBuffer.add("Dealt with a job that failed to retry in a custom way after #{job.arguments.second} attempts. Message: #{error.message}") }
retry_on(AfterDiscardError)
retry_on(ChildAfterDiscardError)
discard_on DiscardableError
discard_on(CustomDiscardableError) { |_job, error| JobBuffer.add("Dealt with a job that was discarded in a custom way. Message: #{error.message}") }
after_discard { |_job, error| JobBuffer.add("Ran after_discard for job. Message: #{error.message}") }
def perform(raising, attempts)
if executions < attempts
JobBuffer.add("Raised #{raising} for the #{executions.ordinalize} time")
raise raising.constantize
else
JobBuffer.add("Successfully completed job")
end
end
end