Skip to content

Commit

Permalink
Merge PR #48010
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaelfranca committed Aug 21, 2023
2 parents 3bfda4c + 659d411 commit 53a3a95
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 1 deletion.
21 changes: 21 additions & 0 deletions activejob/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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*

* Fix deserialization of ActiveSupport::Duration

Previously, a deserialized Duration would return an array from Duration#parts.
Expand Down
30 changes: 30 additions & 0 deletions activejob/lib/active_job/exceptions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 with
Expand Down Expand Up @@ -165,5 +185,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
6 changes: 5 additions & 1 deletion activejob/lib/active_job/execution.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,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(*)
Expand Down
38 changes: 38 additions & 0 deletions activejob/test/cases/exceptions_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require "helper"
require "jobs/retry_job"
require "jobs/after_discard_retry_job"
require "models/person"
require "minitest/mock"

Expand Down Expand Up @@ -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)
[
Expand Down
33 changes: 33 additions & 0 deletions activejob/test/jobs/after_discard_retry_job.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 53a3a95

Please sign in to comment.