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_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.
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.active_job.queue_adapter = :delayed_job
delayed_job backend also requires us to run a migration, which adds
Delayed::Job objects to our database:
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
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:
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!
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
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
touch method is part of ActiveRecord, and allows us to save an ActiveRecord object with the
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_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!
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
1 2 3 4 5 6 7 8 9 10 11
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
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.
- The most important part about setting up a job through ActiveJob is inheriting from
ActiveJob::Base, and implementing a
performmethod. Now, we can actually pass in instances of objects to the
performmethod, rather than ids, which is all thanks to global ids.
- Curious how the
touchmethod 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!