ActiveJob brings benefits but it has just simple features yet.
So, I've made a module with the APIs published officially to add some features of controlling retry.
An Issue about Retry
ActiveJob doesn't have enough features about retrying jobs as of today.
It only provides the
retry_job*1 which enqueues itself again with some options.
An naive sample is here:
class RetryJob < ApplicationJob queue_as :default rescue_from(StandardError) do retry_job(wait: 5.minutes) end def perform(*args) # Do something later end end
Is seems okay. When a job fails for some reason, the job is enqueued and performed in 5 minutes.
However, what if you want to limit the number of retry times? On the above sample, if a job never succeeds, it retries forever. There is, unfortunately, no way to control it with ActiveJob on default settings.
Of course, you can find some gems like ActiveJob::Retry, but there is no dominant gems in this field yet. As far as I can make out, ActiveJob::Retry is the gem gathering stars the most in Github though, it is not enough sophisticated to use in production.
This is an alpha library in active development, so the API may change.
Plus, I feel the gem is kinda thick.
What We Really Want to Do
I think it's gonna be okay if we can set the limit number of retires and find out the number of attempts and whether retry count is exceeded or not.
So, what we want to do are:
- Setting the number of retry limit
- Finding out the attempt number
- Checking whether the retry limit is exceeded or not
class LimitedRetryJob < ApplicationJob queue_as :default retry_limit 5 rescue_from(StandardError) do |exception| raise exception if retry_limit_exceeded? retry_job(wait: attempt_number**2) end def perform(*args) # Do something later end end
Let's implement these methods.
How to Implement it
To tell you the truth, the official document tells us a great idea of that.
serialize and the
deserialize enables us to carry over instance variables which contain serializable objects.
Now, we can implement the above idea like this:
class ApplicationJob < ActiveJob::Base DEFAULT_RETRY_LIMIT = 5 attr_reader :attempt_number class << self def retry_limit(retry_limit) @retry_limit = retry_limit end def load_retry_limit @retry_limit || DEFAULT_RETRY_LIMIT end end def serialize super.merge("attempt_number" => (@attempt_number || 0) + 1) end def deserialize(job_data) super @attempt_number = job_data["attempt_number"] end private def retry_limit self.class.load_retry_limit end def retry_limit_exceeded? @attempt_number > retry_limit end end
If you put this ApplicationJob, you will be able to set a limit on each jobs through
ApplicationJob.retry_limit, get the number of attempts via
ApplicationJob#attempt_number, and check if the retry count exceeds the limit or not calling
- To set the number of retry limit
- To Find out the attempt number
- To check whether the retry limit is exceeded or not
Use in production
I’ve made a module based on this idea for use in production since it’s not a good idea to add methods, which not all subclasses require, to the superclass.
The module puts
ApplicationJob#retry_limit_exceeded? on your jobs.
It only calls APIs declared officially, so it's not easy to brake. Just providing simple methods, you can easily make your own retry logic.
It's dead simple, so you need to implement your own retry features. It never enables jobs to retry themselves automatically.