2017-07-09 13:49:52 -04:00
# frozen_string_literal: true
2017-07-10 09:40:03 -04:00
2016-08-06 12:40:07 -04:00
require " active_support/core_ext/numeric/time "
2016-07-29 17:21:02 -04:00
2016-07-29 16:54:55 -04:00
module ActiveJob
# Provides behavior for retrying and discarding jobs on exceptions.
module Exceptions
extend ActiveSupport :: Concern
module ClassMethods
# Catch the exception and reschedule job for re-execution after so many seconds, for a specific number of attempts.
2016-07-29 16:59:40 -04:00
# If the exception keeps getting raised beyond the specified number of attempts, the exception is allowed to
# bubble up to the underlying queuing system, which may have its own retry mechanism or place it in a
2016-07-29 16:54:55 -04:00
# holding queue for inspection.
#
2016-08-01 19:51:11 -04:00
# You can also pass a block that'll be invoked if the retry attempts fail for custom logic rather than letting
2016-08-16 19:01:59 -04:00
# the exception bubble up. This block is yielded with the job instance as the first and the error instance as the second parameter.
2016-08-01 19:51:11 -04:00
#
2016-07-29 16:54:55 -04:00
# ==== Options
2016-08-02 15:28:16 -04:00
# * <tt>:wait</tt> - Re-enqueues the job with a delay specified either in seconds (default: 3 seconds),
2016-08-01 19:44:29 -04:00
# as a computing proc that the number of executions so far as an argument, or as a symbol reference of
2016-10-13 20:15:51 -04:00
# <tt>:exponentially_longer</tt>, which applies the wait algorithm of <tt>(executions ** 4) + 2</tt>
2016-08-01 19:44:29 -04:00
# (first wait 3s, then 18s, then 83s, etc)
2018-11-23 14:31:14 -05:00
# * <tt>:attempts</tt> - Re-enqueues the job the specified number of times (default: 5 attempts)
2016-07-29 17:23:39 -04:00
# * <tt>:queue</tt> - Re-enqueues the job on a different queue
# * <tt>:priority</tt> - Re-enqueues the job with a different priority
2016-07-29 16:54:55 -04:00
#
# ==== Examples
#
# class RemoteServiceJob < ActiveJob::Base
2016-08-01 19:44:29 -04:00
# retry_on CustomAppException # defaults to 3s wait, 5 attempts
# retry_on AnotherCustomAppException, wait: ->(executions) { executions * 2 }
2019-01-04 19:54:38 -05:00
#
# 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
# # To retry at most 10 times for each individual exception:
# # retry_on Net::OpenTimeout, wait: :exponentially_longer, attempts: 10
# # retry_on Timeout::Error, wait: :exponentially_longer, attempts: 10
#
2018-05-29 03:02:05 -04:00
# retry_on(YetAnotherCustomAppException) do |job, error|
# ExceptionNotifier.caught(error)
2016-08-01 19:51:11 -04:00
# end
2016-07-29 16:54:55 -04:00
#
# def perform(*args)
2016-08-01 19:51:11 -04:00
# # Might raise CustomAppException, AnotherCustomAppException, or YetAnotherCustomAppException for something domain specific
2016-08-20 04:14:47 -04:00
# # Might raise ActiveRecord::Deadlocked when a local db deadlock is detected
2019-01-04 19:54:38 -05:00
# # Might raise Net::OpenTimeout or Timeout::Error when the remote service is down
2016-07-29 16:54:55 -04:00
# end
# end
2018-06-25 18:16:58 -04:00
def retry_on ( * exceptions , wait : 3 . seconds , attempts : 5 , queue : nil , priority : nil )
rescue_from ( * exceptions ) do | error |
2019-04-22 14:39:23 -04:00
executions = executions_for ( exceptions )
2018-11-23 14:31:14 -05:00
2019-04-22 14:39:23 -04:00
if executions < attempts
retry_job wait : determine_delay ( seconds_or_duration_or_algorithm : wait , executions : executions ) , queue : queue , priority : priority , error : error
2016-08-02 15:28:16 -04:00
else
2016-08-01 19:51:11 -04:00
if block_given?
2018-09-23 14:43:48 -04:00
instrument :retry_stopped , error : error do
2018-08-29 13:28:55 -04:00
yield self , error
end
2016-08-01 19:51:11 -04:00
else
2018-09-23 14:43:48 -04:00
instrument :retry_stopped , error : error
2016-08-01 19:51:11 -04:00
raise error
end
2016-07-29 18:07:50 -04:00
end
2016-07-29 16:54:55 -04:00
end
end
# Discard the job with no attempts to retry, if the exception is raised. This is useful when the subject of the job,
# like an Active Record, is no longer available, and the job is thus no longer relevant.
#
2017-09-16 14:59:32 -04:00
# 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.
#
2016-07-29 16:54:55 -04:00
# ==== Example
#
# class SearchIndexingJob < ActiveJob::Base
# discard_on ActiveJob::DeserializationError
2018-05-29 03:02:05 -04:00
# discard_on(CustomAppException) do |job, error|
# ExceptionNotifier.caught(error)
2017-09-16 14:59:32 -04:00
# end
2016-07-29 16:54:55 -04:00
#
# def perform(record)
# # Will raise ActiveJob::DeserializationError if the record can't be deserialized
2017-09-16 14:59:32 -04:00
# # Might raise CustomAppException for something domain specific
2016-07-29 16:54:55 -04:00
# end
# end
2018-06-25 18:16:58 -04:00
def discard_on ( * exceptions )
rescue_from ( * exceptions ) do | error |
2018-09-23 14:43:48 -04:00
instrument :discard , error : error do
yield self , error if block_given?
2017-09-16 14:59:32 -04:00
end
2016-07-29 16:54:55 -04:00
end
end
end
# Reschedules the job to be re-executed. This is useful in combination
# with the +rescue_from+ option. When you rescue an exception from your job
# you can ask Active Job to retry performing your job.
#
# ==== Options
# * <tt>:wait</tt> - Enqueues the job with the specified delay in seconds
# * <tt>:wait_until</tt> - Enqueues the job at the time specified
# * <tt>:queue</tt> - Enqueues the job on the specified queue
# * <tt>:priority</tt> - Enqueues the job with the specified priority
#
# ==== Examples
#
# class SiteScraperJob < ActiveJob::Base
# rescue_from(ErrorLoadingSite) do
# retry_job queue: :low_priority
# end
#
# def perform(*args)
# # raise ErrorLoadingSite if cannot scrape
# end
# end
def retry_job ( options = { } )
2019-10-13 09:01:09 -04:00
instrument :enqueue_retry , options . slice ( :error , :wait ) do
2018-09-16 10:39:55 -04:00
enqueue options
end
2016-07-29 16:54:55 -04:00
end
2016-08-01 19:44:29 -04:00
private
2019-04-12 11:24:05 -04:00
def determine_delay ( seconds_or_duration_or_algorithm : , executions : )
2016-08-02 17:26:56 -04:00
case seconds_or_duration_or_algorithm
2016-08-01 19:44:29 -04:00
when :exponentially_longer
2016-10-28 23:05:58 -04:00
( executions ** 4 ) + 2
2016-08-02 17:26:56 -04:00
when ActiveSupport :: Duration
duration = seconds_or_duration_or_algorithm
duration . to_i
2016-08-01 19:44:29 -04:00
when Integer
2016-08-02 17:26:56 -04:00
seconds = seconds_or_duration_or_algorithm
2016-08-01 19:44:29 -04:00
seconds
when Proc
2016-08-02 17:26:56 -04:00
algorithm = seconds_or_duration_or_algorithm
2016-08-01 19:44:29 -04:00
algorithm . call ( executions )
2016-08-02 17:26:56 -04:00
else
raise " Couldn't determine a delay based on #{ seconds_or_duration_or_algorithm . inspect } "
2016-08-01 19:44:29 -04:00
end
2016-08-02 15:28:16 -04:00
end
2018-09-23 14:43:48 -04:00
2019-04-22 14:39:23 -04:00
def executions_for ( exceptions )
if exception_executions
exception_executions [ exceptions . to_s ] = ( exception_executions [ exceptions . to_s ] || 0 ) + 1
else
# Guard against jobs that were persisted before we started having individual executions counters per retry_on
executions
end
end
2016-07-29 16:54:55 -04:00
end
end