Words and Code

One writer’s journey from words to code.

Working Hard or Hardly Working, Part 2: Custom Jobs

#technicaltuesdays, rails, ruby


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
 rails generate delayed_job:active_record
 rake db:migrate

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
class Order < ActiveRecord::Base
  include AASM
  
  aasm column: 'state', whiny_transitions: false do
      # state machine truncated for brevity!
      state :complete

      event :completed, after: :send_confirmation_email
  end


  def send_confirmation_email
      OrderMailer.confirmation_email(self).deliver_now
  end
end

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
 bin/rails generate job order/confirmation_emailer

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
module Order
  class ConfirmationEmailerJob < ActiveJob::Base
          queue_as :default

      def perform()
          # Do something later
      end
  end
end

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
module Order
  class ConfirmationEmailerJob < ActiveJob::Base
      def perform(order)
        OrderMailer.confirmation_email(order).deliver
      end
  end
end

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
module Order
  class ConfirmationEmailerJob < ActiveJob::Base
      def perform(order)
        OrderMailer.confirmation_email(order).deliver

        order.touch(:confirmation_sent_at)
      end
  end
end

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
class Order < ActiveRecord::Base
  include AASM
  
  aasm column: 'state', whiny_transitions: false do
      state :complete

      event :completed, after do
          Order::ConfirmationEmailerJob.perform_later(self)
      end
  end
end

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
module Order
  class ConfirmationEmailerJob < ActiveJob::Base
      attr_reader :order_id

      def initialize(order_id:)
          @order_id = order_id
      end

      def perform
        OrderMailer.confirmation_email(order).deliver

        order.touch(:confirmation_sent_at)
      end

      private
      def order
          Order.find(order_id)
      end
  end
end

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 a perform method. Now, we can actually pass in instances of objects to the perform 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!