Code Monkey home page Code Monkey logo

activejob-retry's Introduction

ActiveJob::Retry Build Status

This is an alpha library in active development, so the API may change.

Automatic retry functionality for ActiveJob. Just include ActiveJob::Retry.new(strategy: :something, **options) in your job class:

class ProcessWebhook < ActiveJob::Base
  queue_as :webhooks

  # Constant delay between attempts:
  include ActiveJob::Retry.new(strategy: :constant,
                               limit: 3,
                               delay: 5.minutes,
                               retryable_exceptions: [TimeoutError, NetworkError])

  # Or, variable delay between attempts:
  include ActiveJob::Retry.new(strategy: :variable,
                               delays: [1.minute, 5.minutes, 10.minutes, 30.minutes])

  # Or, exponential delay between attempts:
  include ActiveJob::Retry.new(strategy: :exponential, limit: 25)

  # You can also use a custom backoff strategy by passing an object which responds to
  # `should_retry?(attempt, exception)`, and `retry_delay(attempt, exception)`
  # to `retry_with`:
  module ChaoticBackoffStrategy
    def self.should_retry?(retry_attempt, exception)
      [true, true, true, true, false].sample
    end

    def self.retry_delay(retry_attempt, exception)
      (0..10).to_a.sample
    end
  end

  include ActiveJob::Retry.new(strategy: ChaoticBackoffStrategy)

  def perform(webhook)
    webhook.process!
  end
end

The retry will get executed before any rescue_from blocks, which will only get executed if the exception is not going to be retried, or has failed the final retry.

constant options

Option Default Description
limit 1 Maximum number of times to attempt the job (default: 1).
unlimited_retries false If set to true, this job will be repeated indefinitely until in succeeds. Use with extreme caution.
delay 0 Time between attempts in seconds (default: 0).
retryable_exceptions nil A whitelist of exceptions to retry. When nil, all exceptions will result in a retry.
fatal_exceptions [] A blacklist of exceptions to not retry (default: []).

exponential options

Option Default Description
limit 1 Maximum number of times to attempt the job (default: 1).
unlimited_retries false If set to true, this job will be repeated indefinitely until in succeeds. Use with extreme caution.
retryable_exceptions nil Same as for constant.
fatal_exceptions [] Same as for constant.

variable options

Option Default Description
delays required An array of delays between attempts in seconds. The first attempt will occur whenever you originally enqueued the job to happen.
min_delay_multiplier If supplied, each delay will be multiplied by a random number between this and max_delay_multiplier.
max_delay_multiplier The other end of the range for min_delay_multiplier. If one is supplied, both must be.
retryable_exceptions nil Same as for constant.
fatal_exceptions [] Same as for constant.

Callback

All strategies support a callback option:

class ProcessWebhook < ActiveJob::Base
  include ActiveJob::Retry.new(
    strategy: :exponential, limit: 25,
    callback: proc do |exception, delay|
      # will be run before each retry
    end
  )
end

callback must be a proc and is run before each retry. It receives the exception and delay before the next retry as arguments. It is evaluated on instance level, so you have access to all instance variables and methods (for example retry_attempt) of your job.

If the callback returns :halt, retry chain is halted and no further retries will be made:

class ProcessWebhook < ActiveJob::Base
  include ActiveJob::Retry.new(
    strategy: :exponential, limit: 25,
    callback: proc do |exception, delay|
      if some_condition
        :halt # this will halt the retry chain
      end
    end
  )
end

Supported backends

Any queue adapter which supports delayed enqueuing (i.e. the enqueue_at method) will work with ActiveJob::Retry, however some queue backends have automatic retry logic, which should be disabled. The cleanest way to do this is to use a rescue_from in the jobs for which you're using ActiveJob::Retry, so the queue backend never perceives the job as having failed. E.g.:

class MyJob < ActiveJob::Base
  include ActiveJob::Retry.new(strategy: :constant,
                               limit: 3,
                               delay: 5,
                               retryable_exceptions: [TimeoutError, NetworkError])

  queue_as :some_job

  rescue_from(StandardError) { |error| MyErrorService.record(error) }

  def perform
    raise "Weird!"
  end
end

Since rescue_froms are only executed once all retries have been attempted, this will send the recurring error to your error service (e.g. Airbrake, Sentry), but will make it appear to the queue backend (e.g. Que, Sidekiq) as if the job has succeeded.

An alternative is to alter the appropriate JobWrapper to alter the configuration of the backend to disable retries globally. For Sidekiq this would be:

# config/initializers/disable_sidekiq_retries.rb
ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper.sidekiq_options(retry: false)

This has the advantages of moving failed jobs to the Dead Job Queue instead of just executing the logic in the rescue_from, which makes manual re-enqueueing easier. On the other hand it does disable Sidekiq's automatic retrying for all ActiveJob jobs.

Supported Versions

Rails 4.2, 5.0, 5.1, 5.2, and 6.0 are supported, Ruby 2.1+. Other Ruby runtimes (e.g. JRuby, Rubinius) probably work, but are not tested in Travis CI.

Contributing

Contributions are very welcome! Please open a PR or issue on this repo.

activejob-retry's People

Contributors

davydenkovm avatar doits avatar greysteil avatar isaacseymour avatar petehamilton avatar senny avatar trliner avatar troter avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

activejob-retry's Issues

Is there a way to run code in case if all retries are exhausted?

I want to call our error notifier in case when all retries are exhausted

Like this way:

class ProcessWebhook < ActiveJob::Base
  include ActiveJob::Retry.new(
    strategy: :constant,
    limit: 3,
    delay: 5.minutes,
    retries_exhausted: -> (error) { Notifier.call(error.message) } # <--- note this line
  )
  # ...
end

Or this:

class ProcessWebhook < ActiveJob::Base
  # ...

  def perform
     begin
       # do work
     rescue => e
       if retry_rumber == MaxRetries
          Notifier.call(e.message)
       else
         raise
       end
     end
   end
end

Is there a way to run code when retry limit is reached?

You must set `limit: nil` to have unlimited retries

Hi! I use Sidekiq version 4.x and if I set:

constant_retry unlimited_retries: true, delay: 10.seconds

in my ActiveJob I get this error You must set limit: nil to have unlimited retries! If I set unlimited_retries: true limit should be set to nil automatically in the background.

Breaks DelayedJob and Resque

What is the nature and/or behavior of this issue?

This breaks DelayedJob and Resque for some weird ActiveSupport reason.

(I am experiencing an unusual behavior and wondering if this issue may be related)

needed 'require'

To use this gem I had to add 'require active_job/retry in my job.

resque-scheduler dependency

Got an error, To be able to schedule jobs with Resque you need the resque-scheduler gem. Please add it to your Gemfile and run bundle install

Do you need to add it to gem spec? Thanks.

Inheritance issue

Hi,

I just came across a pretty odd issue If I have class inheritance in between my jobs. Let's say I have 2 job classes that are pretty similar, but that I want to run on different queues for priority reasons, I'll make a generic class from which both inherit, and override the queue_as method.

However, if I implement a constant_retry in the mother class, to have it in only one place, I receive those exceptions instead of the retries:

A NoMethodError occurred in background at 2015-12-18 05:28:20 UTC :

undefined method `should_retry?' for nil:NilClass

/srv/http/admin/releases/ab3ca489c158d09f6a4109956c1c3dbd5590e449/vendor/bundle/ruby/2.2.0/gems/activejob-retry-0.5.1/lib/active_job/retry.rb:94:in `rescue_with_handler'

Checking manually, the backoff strategy is indeed not available in the child classes. Any idea how we could inherit retry strategy from the mother class ?

Retry concerns for rails application, sidekiq-like behavior

I have some classes, which could be useful for someone using this great gem!

For example, you would like your jobs to be not-retryable by default, but be retryable only if some retry_options provided.

You could use the following modules/concerns to achieve this goal:

# Some failing job (app/jobs/dummy_job.rb) 

class DummyJob < ActiveJob::Base
  include CommonJobOptions

  retry_options max_retries: 25

  def perform(*)
    raise RuntimeError
  end
end

# app/jobs/concerns/common_job_options.rb 
# Concern for queueing jobs, which also includes sidekiq-like retry concern (CommonRetry)

module CommonJobOptions
  extend ActiveSupport::Concern
  include CommonRetry

  included do
    queue_as do
      self.class.name.underscore.upcase.to_sym
    end
  end
end

# app/jobs/concerns/common_retry.rb
# Concern to make job retryable conditionally (if retry_options provided)
# It is using variable_retry to emulate sidekiq-like behavior for job retrying

module CommonRetry
  extend ActiveSupport::Concern

  # @option options [Fixnum] :max_retries Number of retries
  # @option options [Array of constants] :fatal_exceptions Array of exceptions to be ignored
  # @option options [Array of constants] :retryable_exceptions Array of exceptions to be retried (only)
  # @option options [Proc] :delays_formula Callable function with arity 1
  # @option options [Array of ActiveSupport::Duration] :delays_array Array of delays
  #
  # @example
  #
  # class MyJob
  #   include CommonRetry
  #
  #   retry_options max_retries: 10, fatal_exceptions: [StandardError, RuntimeError]
  #
  #   or
  #
  #   retry_options max_retries: 5, retryable_exceptions: [TimeoutError]

  class_methods do
    def retry_options(options)
      return unless options[:max_retries].present?

      set_options(options)
      include(ActiveJob::Retry)
      variable_retry(delays: delays_array,
                     retryable_exceptions: retryable_exceptions,
                     fatal_exceptions: fatal_exceptions)
    end

    def set_options(options)
      @max_retries = options[:max_retries]
      @retryable_exceptions = options[:fatal_exceptions]
      @fatal_exceptions = options[:retryable_exceptions]
      @delays_formula = options[:delays_formula]
      @delays_array = options[:delays_array]
    end

    def delays_formula(count)
      @delays_formula || -> { ((count ** 4) + 15 + (rand(30)*(count+1))).seconds }
    end

    def delays_array
      @delays_array || (0...max_retries).to_a.map{ |i| delays_formula(i).call }
    end

    def max_retries
      @max_retries || 0
    end

    def retryable_exceptions
      @retryable_exceptions || nil
    end

    def fatal_exceptions
      @fatal_exceptions || []
    end
  end
end

# config/application.rb
config.autoload_paths += Dir["#{config.root}/app/jobs/concerns/"]

# config/sidekiq.yml

:concurrency: 2

:queues:
  - ['DUMMY_JOB', 2]

That's it. Using these concerns you can easily redefine behavior within concrete job using method retry_options with keys :max_retries, :fatal_exceptions, :retryable_exceptions, :delays_formula, :delays_array (see concern CommonRetry).

But if you would like to use default behavior, you can just include concern CommonJobOptions with retry_options max_retries: 5 (see DummyJob)

Undefined method `name` for ActiveJob::QueueAdapters::*

When I tried to include ActiveJob::Retry like below.

class MyJob < ApplicationJob
  include ActiveJob::Retry.new(strategy: :exponential, limit: 25)
...

An exception like below happened.

NoMethodError:
       undefined method `name' for #<ActiveJob::QueueAdapters::SidekiqAdapter:0x007f9c7c00bd68>

ActiveJob::Base.queue_adapter.name doesn't seem to return Class any more. It returns an instance and doesn't have #name.

https://github.com/rails/rails/blob/1f8558fa2707e7707dcfef0aba94de9afcd05d3a/activejob/lib/active_job/queue_adapter.rb

Rails 5.1 support

Hi there! I haven't actually tested this gem, but as Rails doesn't seem to support retry functionality like this, I plan to use it.

I'm also planning on upgrading to Rails 5.1 shortly, though, and as it (Rails 5.1) was released back in April I was wondering if this gem supported it? It doesn't mention that it does on the README (whereas it does mention Rails 4.2 and 5.0, suggesting it doesn't), but it looks like this was last updated in February.

I suppose if it doesn't work on Rails 5.1, this is a request to add that, and if it does, this is a request to update the documentation to reflect that?

Thanks!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.