diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md index f3cae74672..220a69d988 100644 --- a/activejob/CHANGELOG.md +++ b/activejob/CHANGELOG.md @@ -1,3 +1,18 @@ +* Allow a job to retry indefinitely + + The `attempts` parameter of the `retry_on` method now accepts the + symbol reference `:unlimited` in addition to a specific number of retry + attempts to allow a developer to specify that a job should retry + forever until it succeeds. + + class MyJob < ActiveJob::Base + retry_on(AlwaysRetryException, attempts: :unlimited) + + # the actual job code + end + + *Daniel Morton* + * Added possibility to check on `:priority` in test helper methods `assert_enqueued_with` and `assert_performed_with`. diff --git a/activejob/lib/active_job/exceptions.rb b/activejob/lib/active_job/exceptions.rb index 9ff305dedc..2a0d42120d 100644 --- a/activejob/lib/active_job/exceptions.rb +++ b/activejob/lib/active_job/exceptions.rb @@ -25,7 +25,8 @@ module ActiveJob # as a computing proc that takes the number of executions so far as an argument, or as a symbol reference of # :exponentially_longer, which applies the wait algorithm of ((executions**4) + (Kernel.rand * (executions**4) * jitter)) + 2 # (first wait ~3s, then ~18s, then ~83s, etc) - # * :attempts - Re-enqueues the job the specified number of times (default: 5 attempts) + # * :attempts - Re-enqueues the job the specified number of times (default: 5 attempts) or a symbol reference of :unlimited + # to retry the job until it succeeds # * :queue - Re-enqueues the job on a different queue # * :priority - Re-enqueues the job with a different priority # * :jitter - 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) @@ -35,6 +36,7 @@ module ActiveJob # class RemoteServiceJob < ActiveJob::Base # retry_on CustomAppException # defaults to ~3s wait, 5 attempts # retry_on AnotherCustomAppException, wait: ->(executions) { executions * 2 } + # retry_on CustomInfrastructureException, wait: 5.minutes, attempts: :unlimited # # retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3 # retry_on Net::OpenTimeout, Timeout::Error, wait: :exponentially_longer, attempts: 10 # retries at most 10 times for Net::OpenTimeout and Timeout::Error combined @@ -56,7 +58,7 @@ module ActiveJob def retry_on(*exceptions, wait: 3.seconds, attempts: 5, queue: nil, priority: nil, jitter: JITTER_DEFAULT) rescue_from(*exceptions) do |error| executions = executions_for(exceptions) - if executions < attempts + if attempts == :unlimited || executions < attempts retry_job wait: determine_delay(seconds_or_duration_or_algorithm: wait, executions: executions, jitter: jitter), queue: queue, priority: priority, error: error else if block_given? diff --git a/activejob/test/cases/exceptions_test.rb b/activejob/test/cases/exceptions_test.rb index 5555b51b0a..a018b998e9 100644 --- a/activejob/test/cases/exceptions_test.rb +++ b/activejob/test/cases/exceptions_test.rb @@ -300,6 +300,14 @@ class ExceptionsTest < ActiveSupport::TestCase assert_equal ["Raised ActiveJob::DeserializationError for the 5 time"], JobBuffer.values end + test "successfully retry job throwing UnlimitedRetryError a few times" do + RetryJob.perform_later "UnlimitedRetryError", 10 + + assert_equal 10, JobBuffer.values.size + assert_equal "Raised UnlimitedRetryError for the 9th time", JobBuffer.values[8] + assert_equal "Successfully completed job", JobBuffer.values[9] + end + test "running a job enqueued by AJ 5.2" do job = RetryJob.new("DefaultsError", 6) job.exception_executions = nil # This is how jobs from Rails 5.2 will look diff --git a/activejob/test/jobs/retry_job.rb b/activejob/test/jobs/retry_job.rb index 3dfc6f02be..8d8c93fa38 100644 --- a/activejob/test/jobs/retry_job.rb +++ b/activejob/test/jobs/retry_job.rb @@ -17,6 +17,7 @@ class DiscardableError < StandardError; end class FirstDiscardableErrorOfTwo < StandardError; end class SecondDiscardableErrorOfTwo < StandardError; end class CustomDiscardableError < StandardError; end +class UnlimitedRetryError < StandardError; end class RetryJob < ActiveJob::Base retry_on DefaultsError @@ -29,6 +30,7 @@ class RetryJob < ActiveJob::Base retry_on CustomWaitTenAttemptsError, wait: ->(executions) { executions * 2 }, attempts: 10 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(ActiveJob::DeserializationError) { |job, error| JobBuffer.add("Raised #{error.class} for the #{job.executions} time") } + retry_on UnlimitedRetryError, attempts: :unlimited discard_on DiscardableError discard_on FirstDiscardableErrorOfTwo, SecondDiscardableErrorOfTwo