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
.
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.