A queue

image by Hal Gatewood

A job should know whether to run itself

It is a good idea to do as much of the ‘work’ of an application as possible in asynchronous jobs as it means doing less work in each controller action. Rails includes ActiveJob as a way to implement this pattern in your application.

RailsConf 2024

I'm co-chairing RailsConf 2024 in Detroit May 7–9. Come and join us 

Each ActiveJob comes with a selection of callbacks that are run at different points in a job’s lifecycle.

A list of the available callbacks are in the Active Job Rails Guide.

When a job doesn’t always need to be run, we can use these callbacks to save writing conditional logic in every location in the code where we enqueue the job.

Instead of…

…checking whether a job should be enqueued when you enqueue it…

class SendNotificationJob < ApplicationJob
  def perform(user, message)
    # do work
  end
end

if user.wants_notifications?
  SendNotificationJob.perform_later(user, message)
end

Use…

…use an early return to do nothing at the point the job is executed.

class SendNotificationJob < ApplicationJob
  def perform(user, message)
    return unless user.wants_notifications?
    # do work
  end
end

# then
SendNotificationJob.perform_later(user, message)

Or…

…throw the symbol :abort in a callback so a job does not enqueue itself.

class SendNotificationJob < ApplicationJob
  before_enqueue do |job|
    user = job.arguments.first
    throw(:abort) unless user.wants_notifications?
  end

  def perform(user, message)
    # do work
  end
end

# then
SendNotificationJob.perform_later(user, message)

But why?

The code enqueuing the job doesn’t need to care whether the work of the job needs to be done or not. It aids comprehension to keep the logic about when to do something near to the code that actually does the thing. This will make the code easier to understand when you come back to it later.

A job happens an unspecified period of time after it is enqueued. It’s possible the answer to ‘should this job be run?’ could change between the enqueuing and the execution. This is a good candidate for the first, ‘early return’, style.

You should use the second, ‘use throw in a callback’, style when the condition for running a job is very unlikely to change between enqueuing and execution. This is because, with this style, the job is never enqueued if the initial condition isn’t met. This is the exact same logic as the original example.

In both cases the code for the enqueuing is clearer without the external conditional at the point of enqueuing.

Both styles are valid approaches and you can even use them together for different conditions.

Why not?

The code for the ‘use throw in a callback’ style is slightly more complex than an external conditional when you enqueue.

Also it’s only worth encapsulating the ‘should the job be run’ logic if the condition is always applied to the job.

There may be a slight performance cost of enqueuing (and then dequeuing) jobs when using the ‘early return’ style. You might be filling your queue with lots of jobs that don’t do anything.

Brighton Ruby 2024

Still running UK’s friendliest, Ruby event on Friday 28th June. Ice cream + Ruby 


Last updated on May 13th, 2018 by @andycroll

An email newsletter, with one Ruby/Rails technique delivered with a ‘why?’ and a ‘how?’ every two weeks. It’s deliberately brief, focussed & opinionated.