Active Job

Mike Perham edited this page Nov 12, 2016 · 26 revisions

Active Job Introduction

Rails 4.2 introduced Active Job. Active Job is a standard interface for interacting with job runners. Active Job can be configured to work with Sidekiq.

Note that more advanced Sidekiq features (sidekiq_options) cannot be controlled or configured via ActiveJob, e.g. saving backtraces.

Active Job Setup

The Active Job adapter must be set to :sidekiq or it will simply use the default :inline. This can be done in config/application.rb like this:

class Application < Rails::Application
  # ...
  config.active_job.queue_adapter = :sidekiq
end

We can use the generator to create a new job.

rails generate job Example

This above command will create /app/jobs/example_job.rb

class ExampleJob < ActiveJob::Base
  # Set the Queue as Default
  queue_as :default

  def perform(*args)
    # Perform Job
  end
end

Usage

Jobs can be added to the job queue from anywhere. We can add a job to the queue by:

ExampleJob.perform_later args

At this point, Sidekiq will run the job for us. If the job for some reason fails, Sidekiq will retry as normal.

Customizing error handling

Activejob does not support the full richness of Sidekiq's retry feature. Instead it has a simple abstraction for encoding retries upon encountering specific exceptions.

class ExampleJob < ActiveJob::Base
  rescue_from(ErrorLoadingSite) do
    retry_job wait: 5.minutes, queue: :low_priority 
  end 

  def perform(*args)
    # Perform Job
  end
end

You can also selectively retry depending on the exception type or exception arguments:

  rescue_from(ErrorLoadingSite) do |exception|
    # do what you wish with the exception here  
  end

Action Mailer

Action Mailer now comes with a method named #deliver_later which will send emails asynchronously (your emails send in a background job). As long as Active Job is setup to use Sidekiq we can use #deliver_later. Unlike Sidekiq, using Active Job will serialize any activerecord instance with Global ID. Later the instance will be deserialized.

Mailers are queued in the queue mailers. Remember to start sidekiq processing that queue:

bundle exec sidekiq -q default -q mailers

To send a basic message to the Job Queue we can use:

UserMailer.welcome_email(@user).deliver_later

If you would like to bypass the job queue and perform the job synchronously you can use:

UserMailer.welcome_email(@user).deliver_now

With Sidekiq we had the option to send emails with a set delay. We can do this through Active Job as well.

Old syntax for delayed message in Sidekiq:

UserMailer.delay_for(1.hour).welcome_email(@user.id)
UserMailer.delay_until(5.days.from_now).welcome_email(@user.id)

New syntax to send delayed message through Active Job:

UserMailer.welcome_email(@user).deliver_later(wait: 1.hour)
UserMailer.welcome_email(@user).deliver_later(wait_until: 10.hours.from_now)

Limitations

GlobalID, which ActiveJob uses, allows serializing full ActiveRecord objects as an argument to #perform, so that

def perform(user_id)
  user = User.find(user_id)
  user.send_welcome_email!
end

can be replaced with

def perform(user)
  user.send_welcome_email!
end

Unfortunately this means that if the User record is deleted after the job is enqueued but before the perform method is called, exception handling is different. With regular Sidekiq, you could handle this with

def perform(user_id)
  user = User.find_by(id: user_id)

  if user
    user.send_welcome_email!
  else
    # handle a deleted user record
  end
end

With ActiveJob, the perform(user) will instead raise for a missing record exception as part of deserializing the User instance.

You can work around this with

class MyJob < ActiveJob::Base
  rescue_from ActiveJob::DeserializationError do |exception|
    # handle a deleted user record
  end

  # ...
end

Job ID

ActiveJob has its own Job ID which means nothing to Sidekiq. In Rails 5 and later, you will be able to get Sidekiq's JID, by using provider_job_id:

job = SomeJob.perform_later
jid = job.provider_job_id