21
votes

From the Rails API, I found ActiveJob can retry_job interval:

my_job_instance.enqueue
my_job_instance.enqueue wait: 5.minutes
my_job_instance.enqueue queue: :important
my_job_instance.enqueue wait_until: Date.tomorrow.midnight

http://api.rubyonrails.org/classes/ActiveJob/Enqueuing.html

But if I want to set retry count, such as Sidekiq's:

include Sidekiq::Worker
sidekiq_options :retry => 5

https://github.com/mperham/sidekiq/wiki/Error-Handling

How to do in this sample code?

class SiteScrapperJob < ActiveJob::Base
  rescue_from(ErrorLoadingSite) do
    retry_job queue: :low_priority
  end

  def perform(*args)
    # raise ErrorLoadingSite if cannot scrape
  end
end

Now I added this to my job class:

Sidekiq.default_worker_options = { retry: 5 }

But it seems not very good.

7

7 Answers

21
votes

As of Sidekiq 6.0.4 you can use sidekiq_options in an ActiveJob to set the retry option.

19
votes

You also might be interested in this solution which uses serialize and deserialize api to store the number of attempts.

class DeliverWebhookJob < ActiveJob::Base
  def serialize
    super.merge('attempt_number' => (@attempt_number || 0) + 1)
  end

  def deserialize(job_data)
    super
    @attempt_number = job_data['attempt_number']
  end

  rescue_from(ErrorLoadingSite) do |exception|
    retry_job(wait: 10) if @attempt_number < 5
  end

  def perform(*args)
    # raise ErrorLoadingSite if cannot scrape
  end
end

Take it from here.

15
votes

Since Rails 5.1, there is a built-in way to do this using the retry_on method. It's a general ActiveJob method, so it will work with any queuing backend, not just Sidekiq.

For example, for your specific job you could do:

class SiteScraperJob < ActiveJob::Base
  retry_on ErrorLoadingSite, queue: :low_priority, attempts: 5

  def perform(*args)
    # raise ErrorLoadingSite if cannot scrape
  end
end

You can also set a constant wait interval or an exponential wait strategy, as explained in the docs.

4
votes

There is a activejob-retry gem which does the job

class SiteScrapperJob < ActiveJob::Base
  include ActiveJob::Retry.new(limit: 5, strategy: :exponential)

  def perform(*args)
    # raise ErrorLoadingSite if cannot scrape
  end
end

Another option is to use sidekiq middleware:

First define job_options class-method which will be available in the subclasses:

class ApplicationJob < ActiveJob::Base
  def self.job_options(options)
    @job_options = options
  end

  def self.get_job_options
    @job_options || {}
  end
end

Add middleware which reads job_options from the jobs's class and writes them to the job item for sidekiq:

module Sidekiq
 class JobOptionsMiddleware

   def call(job_wrapper, item, queue, redis_pool)
     job = item['args'][0]['job_class'].constantize

     job.get_job_options
       .each{ |option, value| item[option] = value if item[option].nil? }

     yield
   end

 end

 # in sidekiq initializer

 Sidekiq.configure_client do |config|
   config.client_middleware do |chain|
     chain.add Sidekiq::JobOptionsMiddleware
   end
 end

And finally

 class SiteScrapperJob < ApplicationJob
   job_options retry: 5

   def perform
     # your code
   end
 end
2
votes

See here the defaults for Sidekiq. The attribute retry "accepts" a boolean value and not a number as you assumed.

From the merge of active_job into Rails this other file one can see that once again retry doesn't accept the number of retries.

What the documentation says then is that per job you can define if the job retries or not.

I also tried to find if the config/sidekiq.yml file can receive this number, and seems like it can't.

Finally,

If you don't fix the bug within 25 retries (about 21 days), Sidekiq will stop retrying and move your job to the Dead Job Queue. You can fix the bug and retry the job manually anytime within the next 6 months using the Web UI.

1
votes

FYI this problem has been fixed in the release of Sidekiq 6.0. You can have a look at the changelog here: https://github.com/mperham/sidekiq/blob/master/Changes.md

But basically you can pass the same options by calling sidekiq_options with your options hash. Thanks Mike Perham.

0
votes

if you just use sidekiq, nerver change backend, monkey patch can help you

module ActiveJob
  module QueueAdapters
    class SidekiqAdapter
      def enqueue(job)
        JobWrapper.sidekiq_options job.sidekiq_options_hash if job.sidekiq_options_hash
        JobWrapper.sidekiq_retry_in job.sidekiq_retry_in_block if job.sidekiq_retry_in_block
        Sidekiq::Client.push(
          'class' => JobWrapper,
          'wrapped' => job.class.to_s,
          'queue' => job.queue_name,
          'args'  => [ job.serialize ]
        )
      end

      def enqueue_at(job, timestamp)
        JobWrapper.sidekiq_options job.sidekiq_options_hash if job.sidekiq_options_hash
        JobWrapper.sidekiq_retry_in job.sidekiq_retry_in_block if job.sidekiq_retry_in_block
        Sidekiq::Client.push(
          'class' => JobWrapper,
          'wrapped' => job.class.to_s,
          'queue' => job.queue_name,
          'args'  => [ job.serialize ],
          'at'    => timestamp
        )
      end
    end
  end

  class Base
    class_attribute :sidekiq_options_hash
    class_attribute :sidekiq_retry_in_block

    def self.sidekiq_options(opts={})
      self.sidekiq_options_hash = opts
    end

    def self.sidekiq_retry_in(&block)
      self.sidekiq_retry_in_block = block
    end
  end
end

then, you can write like below:

class BaseJob < ActiveJob::Base

  sidekiq_options retry: 2, queue: :low
  sidekiq_retry_in { |count, _| 3 * count }

  def perform; end
end

happy coding