Skip to content

Commit 7df1b84

Browse files
authored
Merge pull request rails#54541 from Shopify/report-option-retry-on
Add `report:` option to `ActiveJob::Base#retry_on` and `#discard_on`
2 parents 7d786ea + 3ff8c45 commit 7df1b84

File tree

5 files changed

+36
-6
lines changed

5 files changed

+36
-6
lines changed

activejob/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
* Add `report:` option to `ActiveJob::Base#retry_on` and `#discard_on`
2+
3+
When the `report:` option is passed, errors will be reported to the error reporter
4+
before being retried / discarded.
5+
6+
*Andrew Novoselac*
7+
18
* Accept a block for `ActiveJob::ConfiguredJob#perform_later`.
29

310
This was inconsistent with a regular `ActiveJob::Base#perform_later`.

activejob/lib/active_job/exceptions.rb

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ module ClassMethods
3434
# * <tt>:queue</tt> - Re-enqueues the job on a different queue
3535
# * <tt>:priority</tt> - Re-enqueues the job with a different priority
3636
# * <tt>:jitter</tt> - A random delay of wait time used when calculating backoff. The default is 15% (0.15) which represents the upper bound of possible wait time (expressed as a percentage)
37+
# * <tt>:report</tt> - Errors will be reported to the Rails.error reporter before being retried
3738
#
3839
# ==== Examples
3940
#
@@ -49,8 +50,9 @@ module ClassMethods
4950
# # retry_on Net::ReadTimeout, wait: 5.seconds, jitter: 0.30, attempts: 10
5051
# # retry_on Timeout::Error, wait: :polynomially_longer, attempts: 10
5152
#
52-
# retry_on(YetAnotherCustomAppException) do |job, error|
53-
# ExceptionNotifier.caught(error)
53+
# retry_on YetAnotherCustomAppException, report: true
54+
# retry_on EvenWorseCustomAppException do |job, error|
55+
# CustomErrorHandlingCode.handle(job, error)
5456
# end
5557
#
5658
# def perform(*args)
@@ -59,10 +61,11 @@ module ClassMethods
5961
# # Might raise Net::OpenTimeout or Timeout::Error when the remote service is down
6062
# end
6163
# end
62-
def retry_on(*exceptions, wait: 3.seconds, attempts: 5, queue: nil, priority: nil, jitter: JITTER_DEFAULT)
64+
def retry_on(*exceptions, wait: 3.seconds, attempts: 5, queue: nil, priority: nil, jitter: JITTER_DEFAULT, report: false)
6365
rescue_from(*exceptions) do |error|
6466
executions = executions_for(exceptions)
6567
if attempts == :unlimited || executions < attempts
68+
ActiveSupport.error_reporter.report(error, source: "application.active_job") if report
6669
retry_job wait: determine_delay(seconds_or_duration_or_algorithm: wait, executions: executions, jitter: jitter), queue: queue, priority: priority, error: error
6770
else
6871
if block_given?
@@ -82,6 +85,8 @@ def retry_on(*exceptions, wait: 3.seconds, attempts: 5, queue: nil, priority: ni
8285
# Discard the job with no attempts to retry, if the exception is raised. This is useful when the subject of the job,
8386
# like an Active Record, is no longer available, and the job is thus no longer relevant.
8487
#
88+
# Passing the <tt>:report</tt> option reporter the error through the error reporter before discarding the job.
89+
#
8590
# You can also pass a block that'll be invoked. This block is yielded with the job instance as the first and the error instance as the second parameter.
8691
#
8792
# +retry_on+ and +discard_on+ handlers are searched from bottom to top, and up the class hierarchy. The handler of the first class for
@@ -91,18 +96,20 @@ def retry_on(*exceptions, wait: 3.seconds, attempts: 5, queue: nil, priority: ni
9196
#
9297
# class SearchIndexingJob < ActiveJob::Base
9398
# discard_on ActiveJob::DeserializationError
94-
# discard_on(CustomAppException) do |job, error|
95-
# ExceptionNotifier.caught(error)
99+
# discard_on CustomAppException, report: true
100+
# discard_on(AnotherCustomAppException) do |job, error|
101+
# CustomErrorHandlingCode.handle(job, error)
96102
# end
97103
#
98104
# def perform(record)
99105
# # Will raise ActiveJob::DeserializationError if the record can't be deserialized
100106
# # Might raise CustomAppException for something domain specific
101107
# end
102108
# end
103-
def discard_on(*exceptions)
109+
def discard_on(*exceptions, report: true)
104110
rescue_from(*exceptions) do |error|
105111
instrument :discard, error: error do
112+
ActiveSupport.error_reporter.report(error, source: "application.active_job") if report
106113
yield self, error if block_given?
107114
run_after_discard_procs(error)
108115
end

activejob/test/cases/exceptions_test.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,18 @@ def adapter_skips_scheduling?(queue_adapter)
343343
assert_equal ["Raised DefaultsError for the 5th time"], JobBuffer.values
344344
end
345345

346+
test "retrying a job reports error when report: true" do
347+
assert_error_reported(ReportedError) do
348+
RetryJob.perform_later("ReportedError", 2)
349+
end
350+
end
351+
352+
test "discarding a job reports error when report: true" do
353+
assert_error_reported(AfterDiscardRetryJob::ReportedError) do
354+
AfterDiscardRetryJob.perform_later("AfterDiscardRetryJob::ReportedError", 2)
355+
end
356+
end
357+
346358
test "#after_discard block is run when an unhandled error is raised" do
347359
assert_raises(AfterDiscardRetryJob::UnhandledError) do
348360
AfterDiscardRetryJob.perform_later("AfterDiscardRetryJob::UnhandledError", 2)

activejob/test/jobs/after_discard_retry_job.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class DiscardableError < StandardError; end
1111
class CustomDiscardableError < StandardError; end
1212
class AfterDiscardError < StandardError; end
1313
class ChildAfterDiscardError < AfterDiscardError; end
14+
class ReportedError < StandardError; end
1415

1516
retry_on DefaultsError
1617
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}") }
@@ -19,6 +20,7 @@ class ChildAfterDiscardError < AfterDiscardError; end
1920

2021
discard_on DiscardableError
2122
discard_on(CustomDiscardableError) { |_job, error| JobBuffer.add("Dealt with a job that was discarded in a custom way. Message: #{error.message}") }
23+
discard_on(ReportedError, report: true)
2224

2325
after_discard { |_job, error| JobBuffer.add("Ran after_discard for job. Message: #{error.message}") }
2426

activejob/test/jobs/retry_job.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class FirstDiscardableErrorOfTwo < StandardError; end
1818
class SecondDiscardableErrorOfTwo < StandardError; end
1919
class CustomDiscardableError < StandardError; end
2020
class UnlimitedRetryError < StandardError; end
21+
class ReportedError < StandardError; end
2122

2223
class RetryJob < ActiveJob::Base
2324
retry_on DefaultsError
@@ -31,6 +32,7 @@ class RetryJob < ActiveJob::Base
3132
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}") }
3233
retry_on(ActiveJob::DeserializationError) { |job, error| JobBuffer.add("Raised #{error.class} for the #{job.executions} time") }
3334
retry_on UnlimitedRetryError, attempts: :unlimited
35+
retry_on ReportedError, report: true
3436

3537
discard_on DiscardableError
3638
discard_on FirstDiscardableErrorOfTwo, SecondDiscardableErrorOfTwo

0 commit comments

Comments
 (0)