Skip to content

SleeplessByte/resumable_job

Repository files navigation

ResumableJob

Build Status: master Gem Version MIT license Maintainability

Make any ActiveJob resumable.

Use exception flow to make jobs exceptionally resumable, whilst retaining other state, with automatic exponential backoff handling. ActiveJob is not a dependency, so this could be used with "anything". Adds a module to include somewhere that adds a method which yields a block. During this block, you can throw a ResumableJob::ResumeLater to call the following:

self.class
    .set(wait_until: resume_at || ResumableJob::Backoff.to_time(attempt))
    .perform_later(pause(state).merge(attempt: attempt + 1))

State is passed through pause and when pause is not overridden will be all the arguments you passed to your job plus an attempt argument that is steadily increased in order to to exponential backoff.

Installation

Add this line to your application's Gemfile:

gem 'resumable_job'

And then execute:

$ bundle

Or install it yourself as:

$ gem install resumable_job

Usage

Make a job resumable

Simple example to implement pagination that resumes later if you receive a "Rate Limit Exceeded".

class FetchDataJob < ApplicationJob
  include ResumableJob::Resumable
  
  def perform(state)
    page = state.fetch(:page) { 1 }
    
    resumable(state) do
      loop do
        result = DataFetcher.call(page: page)
        raise ResumableJob::ResumeLater(state: state.merge(page: page)) if result.status == 429
        break unless result.next_page?
          
        page = result.next_page
      end
    end
  end
end

Turn inner exception into resumable

When the exception has more information (for example a "rate limit reset" value), it can be turned into a resume later. Additionally, the state of the resume later exception will me merged into the original state, and then into the pause state.

class FetchDataJob < ApplicationJob
  include ResumableJob::Resumable

  def perform(state)
    resumable(state) do
      fetch_data(state)
    end
  end

  private

  def fetch_data(state)
    RateLimitableFetcher.call(state)
  rescue RateLimitableFetcher::RateLimited => ex
    raise ResumableJob::ResumeLater.new(state: state, utc: ex.retry_at, message: ex.message)
  end
end

Filter out keys from the state

Some state is not serializable. You may be calling your job with perform_now, but when it resumes later, some arguments can not be serialized. Use the pause override to include state not originally present, modify state that is not passed by your exception (ResumeLater exception state), or remove state.

class FetchDataJob < ApplicationJob
  include ResumableJob::Resumable
  
  def pause(state)
    state.slice(:attempt, :page, :token)
  end
end

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/SleeplessByte/resumable_job.