This blog post is part of a series on Active Job. Read Part 1 here.
With the advent of Rails 4.2, one thing is definitely for sure: there is now one background job to rule them all: Active Job. Last week, I learned about Active Job’s easy integration with ActionMailer. But, as nice as it is to have those simple deliver_now
and deliver_later
methods, there will inevitably be a time that we want to do something more — something that requires writing our own custom job.
Active Job is, thankfully, very good at letting us do this. Since my ActionMailer post last week, I’ve written a few jobs using Active Job’s framework. And each time that I’ve done it, it’s gotten easier and easier. Of course, not all of my jobs have been super complex, but once I understood the basics, I could look at other people’s code and understand how it was structure and what exactly was going on.
The only way to get comfortable writing my own custom jobs was by – wait for it – actually writing one! So that’s exactly what we’ll do together. Let’s turn our ActionMailer method from last week into its own job that will be able to run asychronously. Hold on to your hats, because we’re about to leave the shire.
Job Generating
The most important first step before even generating a job is to make sure that we have our queue adapter set up for Active Job. The default queue adapter for Active Job is to run inline (or, within the same request-response cycle), which means that it will not run in the background. One of the lovely things about Active Job is that we can use any queueing backend that we prefer, as long as we follow the documentation to set it up. Last week, we did this by adding delayed_job
to our Gemfile
, and setting our queueing configurations inside of config/application.rb
:
config.active_job.queue_adapter = :delayed_job
The delayed_job
backend also requires us to run a migration, which adds Delayed::Job
objects to our database:
1 2 |
|
This will be important later on, because the only way for us to see any jobs that are enqueued or that have failed is by calling Delayed::Job.all
in the console or from within the context of a controller. This migration also adds helpful columns to our delayed_jobs
table, including priority
, attempts
, run_at
, failed_at
, and last_error
. This data would be particularly relevant if we wanted to allow a job to be re-run, or for a job’s errors to be displayed within an admin panel.
Now that we have all of our queueing backend setup taken care of, we can start to write our job. At the moment, we have an instance method called send_confirmation_email
on our Order
class, which uses deliver_now
to send an email. You’ll remember that we’re calling this method from within a state machine:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
We still want to use our OrderMailer
, but it would be nice to be able to do that within the context of a background job that exists in its own file, so that we can customize it. Let’s generate our job and text unit for our order confirmation emailer, with a nice namespace to boot:
1
|
|
Now, inside of app/jobs/order/confirmation_emailer.rb
, we have a simple little file that looks like this:
1 2 3 4 5 6 7 8 9 |
|
It doesn’t look like much, does it? But, it’s honestly almost all that we need. The most important thing to know about ActiveJob when it comes to writing a job is this: you must have a perform
method. And, as you might expect, the perform
method should be, well, responsible for actually performing the job. However, our job doesn’t do anything yet. And we’re not even calling it anywhere! You know what that means, right? It’s time for us to set off on our custom job adventure and start writing!
Job Writing
Since we already know that our perform
method is going to be responsible for performing our job, we know that this is where all of our logic should go. It would be nice if we could just pass this background job an order
instance, and then tell it what to do with that order. Our OrderMailer
has a confirmation_email
method that accepts an order
object, so we can really just use the mailer inside of our job.
Let’s pass an order
to our job, and then have the job be responsible for delivering the confirmation email:
1 2 3 4 5 6 7 |
|
Nice! That was easy enough, right? You’ll notice that our ConfirmationEmailerJob
inherits from ActiveJob::Base
. This is very important, because without inherting from this module, our job would have no idea what to do with its perform
method! It’s crucial to keep this in mind particularly if we are manually creating our jobs and not using the rails generator; in that case, we need to add the ActiveJob::Base
inheritance on our own. (I was bit by this recently, so don’t make the same mistake that I did!)
Honestly though, this isn’t doing that much more than what our OrderMailer
did initially. We’re writing a custom job, so let’s customize what this job can do. In addition to delivering our confirmation email, it would be cool if this job could also update an attribute on our order
called confirmation_sent_at
. This is just a datetime format attribute that will probably end up in an admin panel or dashboard. And there’s a really elegant way that we can update this attribute from within the job:
1 2 3 4 5 6 7 8 9 |
|
The touch
method is part of ActiveRecord, and allows us to save an ActiveRecord object with the updated_at
and updated_on
attributes set to the current date and time. It’s important to note that there are no validations that are performed by this method, and it’s actually only the after_touch
, after_commit
, and after_rollback
ActiveRecord callbacks that are ever executed.
If we called order.touch
, we would only update order.updated_at
. But, since we have a specific attribute called confirmation_sent_at
in order to specifically keep track of our confirmation emails, we can tell the touch
method to update that attribute by passing it in as an parameter: order.touch(:confirmation_sent_at)
. This is a pretty awesome method, but don’t make the mistake of trying to call it on a plain old Ruby object, or on an unsaved ActiveRecord object! The object must be persisted, since the touch
method is defined in the ActiveRecord::Persistence
module. Otherwise, you’ll get an ActiveRecordError, and we don’t have time for that silliness!
However, what we do need to do next is call our background job and have it…well, do it’s job!
Job Winning
Now that we have our Order::ConfirmationEmailerJob
class ready to get to work, it’s time for us to actually get to work and start performing. Since we already have our state machine in place, let’s just call our job from within our Order
class:
1 2 3 4 5 6 7 8 9 10 11 |
|
Nice! The perform_later
method on our Order::ConfirmationEmailerJob
will instantiate our job and call perform
on it. Since we’re already in the context of the Order
model, we can simply pass in self
, which is just the order
instance, into our job, which will know exactly what to do with it. We’re also taking advantage of the after
callback in our state machine, and invoking our job directly inside of our completed
event. Alternatively, we could have abstracted this out into a method for a more granular separation of concerns. But since our job is pretty simple, it also makes sense to put it directly into the after
block.
Now, when we call order.completed!
, our state machine will transition our order
object to the state 'complete'
, and after the event, it will create a new instance of our Order::ConfirmationEmailerJob
, which will call the perform
method asychronously, and will use delayed_job
to enqueue the job in the background. The emailer job would then send our order confirmation email using ActionMailer, and then it would update the confirmation_sent_at
attribute on our order
instance. And, if we wanted to see what the job looked like while it was being running asychronously, we could open up the rails console and run Delayed::Job.last
, which would show us all the details about the most recent job that we had called.
Wow, that’s a lot of things happening in a pretty complex sequence! That tiny little perform
method isn’t looking so tiny after all, is it?
Interestingly, the job that we wrote is still a lot simpler than how Rails jobs used to be written. Before Active Job was integrated into Rails 4.2, we weren’t able to pass in an order
instance into the perform
method of our job. Instead, we had to pass in the id
of an object, and then look it up inside of our job:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Not as nice as clean as what we wrote initially, right? But still, despite all the extra lines of code, it’s pretty amazing that all of these actions can be performed asynchronously, and within different request-response cycles. Rails’ ActiveJob truly does rule all.
tl;dr?
- The most important part about setting up a job through ActiveJob is inheriting from
ActiveJob::Base
, and implementing aperform
method. Now, we can actually pass in instances of objects to theperform
method, rather than ids, which is all thanks to global ids. - Curious how the
touch
method works? Check out the documentation on this amazing little function. - Here’s a great railscast on setting up a job – keep in mind though, it’s pre-Rails 4.2 and ActiveJob!