ActiveJob is Rails’ way of handling background jobs, providing a common interface around various queuing backends that do the actual processing (Sidekiq, Resque, etc). ActiveJob’s interface is pretty simple, you call #perform_later on the job, that job gets added to the queue, and at some point, a worker process will pick it up and execute it.

You can also set various options via the set method, for example:

  ThingJob.set(wait: 1.day).perform_later

will cause the job to be executed one day from now. In this post, I want to walk through my process of finding a way to set a default wait time for a particular job.

Background

As part of GitArborist I have a background job that processes pull request statuses to check if the PR has a success status (i.e. CI has passed etc). The problem is, it’s conceivable that this job could run before any other external services have managed to register their status, meaning I might think the PR state is success when actually CI status is still pending but that status has not yet been added to Github.

My simple solution: Just delay the job to give any external services time to register their status(es). All well and good, but I always want this job delayed, and ideally, I don’t want to have to remember that everywhere I call it in the code.

Reading the Rails Documentation

The first port-of-call on this journey is Rails’ documentation on ActiveJob. I scan through it but find no mention of default values for wait. I do, however, see that there are some callbacks available. before_enqueue sounds like what I want, as I can then ensure the wait time is set before the job gets added to the queue.

class MyDelayedJob < ApplicationJob
  before_enqueue { |job| <set wait time ???> }
end

Now the question becomes: “How do I set the wait time?”. job in the callback is the actual MyDelayedJob instance, but the way ActiveJob works is that set(...) is a class method. Time to dig into the source.

Reading the Rails Source

I won’t even pretend to be a Rails source expert here, but for simple things like this, the trail is usually not too difficult. First, let’s figure out where the set method lives so we can see what it’s doing.

# Rails console
> ApplicationJob.method(:set).source_location
=> ["<omitted_path>/activejob-6.0.2.1/lib/active_job/core.rb", 74]

Aha, now we can jump to core.rb source on Github. It seems that set just creates a new ConfiguredJob. So, what does ConfiguredJob do in its initializer?

def initialize(job_class, options = {})
  @options = options
  @job_class = job_class
end

Then down on line 16, we see where @options is used:

def perform_later(*args)
  @job_class.new(*args).enqueue @options
end

So the enqueue method is what we want to track down next. Looking in the active_job directory there’s a likely candidate: enqueueing.rb. Track down the enqueue method and we see exactly what we’re looking for:

  self.scheduled_at = options[:wait].seconds.from_now.to_f if options[:wait]

Snooping around a bit confirms that in core.rb there’s attr_accessor :scheduled_at.

Trouble managing your Github Pull Requests?

GitArborist was created to simplify Pull Request management on Github
Mark PR dependencies, automatic merging, scheduled merges, and more →

Adding the Default Wait Time

So putting together what we’ve learned above, we can use job.scheduled_at = <time> to set the wait time. My final implementation looks like this:

class MyDelayedJob < ApplicationJob
  before_enqueue { |job| job.scheduled_at ||= (Time.current + 1.minute).to_f }
  
  def perform
    ...
  end
end

The ||= here means the time can be set if we want, otherwise, it will use this default time. You could also do things like enforcing a minimum or maximum time if that was preferable in your application.

Source Spelunking

While setting a default wait time for ActiveJob jobs is probably a niche requirement, my aim here is more to show the process I went through as a general approach that can be used to tackle lots of questions in the Rails world. source_location, Rails guides, and the Rails source are valuable resources for any Rails developer.