New Rails Shiny: ActiveJob
One highly anticipated feature of Rails 4.2 is the introduction of the ActiveJob library. Like many components in Rails, ActiveJob serves as the adapter layer on a few of the most popular queuing libraries in the Ruby ecosystem.
With the new ActiveJob library, choosing a queuing library with an unique API will not be something to worry about anymore. Rails now provides a unique queuing interface which allows you to swap out queueing gems to your heart’s desire, without changing your application code. Want to switch from Delayed Job to Backburner? Not a problem. ActiveJob let’s you do that with minimal pain.
Another great feature is complete integration with ActionMailer. Email is always one of those tasks that can be done without the user needing to know the email was sent. ActiveJob also provides the same level of abstraction with the methods deliver_later!
and deliver_now!
, with the obvious functionality.
Lastly, with the addition of ActiveJob, Rails 4.2 will also include the Global ID library, which provides a unique identifier of a model instance: gid://Application/Model/id
. This is particularly useful in job scheduling, since we need to reference a model object rather than the serialized object itself. Instead of the scheduler being involved with the particular model and it’s ID, the scheduler just needs to use the global ID to find the exact model instance.
To fully understand the ActiveJob feature set, we need to take a look at it’s core ActiveJob functionality, the ActionMailer functionality, and Global ID.
Using ActiveJob
For this example, we will be utilizing a Rails 4.2 application (4.2.0.beta2 to be exact) that deals with geolocation. In the application, there is an Account model with fields for zipcode, city, state, latitude, and longitude. Since zipcode yields the results for the other items in the list, this is the only one our application will request from the user.
Below is an example of our account class:
{
"id": null,
"name": null,
"city": null,
"state": null,
"zipcode": null,
"latitude": null,
"longitude": null,
"created_at": null,
"updated_at": null
}
Without a job based system, the simple task of finding this additional data might be too slow of a transaction for the application to do in real time. Typically, the application must go out to a third party service to gather the data. This is the perfect scenario for a background job.
Let’s create our first job for this action!
First, be sure to add gem 'delayed_job_active_record'
to your gemfile, run bundle install
in the terminal, and follow the directions on the delayed_job github page to fully install Delayed Job.
Once Delayed Job (or your queuing system of choice) is installed, you’ll need to generate the job class from the terminal. By using the following command, a new file will appear in the app/jobs folder:
$ rails g job geolocate_account
create app/jobs/geolocate_account_job.rb
The above command generates the following class:
class GeolocateAccountJob < ActiveJob::Base
queue_as :default
def perform(*args)
# Do something later
end
end
In the above class, the perform
action is where all the logic for the task will live. For our application, we want to find the geolocation from the zipcode. We will utilize the geocoder gem, which gives us an API to connect to a third parties geolocation service. Here’s the code:
def perform(account)
result = Geocoder.search(account.zipcode).first
if result
account.latitude = result.latitude
account.longitude = result.longitude
account.city = result.city
account.state = result.state
account.save!
end
end
Now that we have our job’s perform
method complete. Let’s open the console and call this job to get our first account queued up. Since our application is fresh, let’s add a new account first:
Account.create(name: 'Dunder Mifflin', zipcode: '18505')
And then queue up the account with our GeolocateAccountJob
:
GeolocateAccountJob.perform_later Account.first
You can see that the job has successfully been enqueued by running Delayed::Job.count
. You can also review your logs and see that the job has successfully committed to the database.
Queueing for the Future
Sometimes it is important to be able to queue a job in the future and ActiveJob let’s you do just that. For example, the vanilla way of queueing a job is as follows:
GeolocateAccountJob.perform_later Account.first
However, you also have the ability to queue jobs for the future:
GeolocateAccountJob.set(wait: Date.tomorrow.noon).perform_later Account.first
and
GeolocateAccountJob.set(wait_until: 1.day).perform_later Account.first
Both of these code snippets will yield the about same results.
You can also set a priority to your queue jobs:
GeolocateAccountJob.set(queue: :low_priority).perform_later Account.first
Rescuing from Failure
Another great feature of ActiveJob is the ability to catch exceptions that happen in your perform
method. Using the same GeolocateAccountJob
class, let’s modify our perform
method to throw a ServiceDown
exception:
class GeolocateAccountJob < ActiveJob::Base
queue_as :default
def perform(account)
raise Geolocation::ServiceDown
end
end
We can then add a rescue block to handle our exception:
class GeolocateAccountJob < ActiveJob::Base
queue_as :default
rescue_from(Geolocation::ServiceDown) do |exception|
# Handle failed exception
end
def perform(account)
raise Geolocation::ServiceDown
end
end
Integration with ActionMailer
Another beneficial feature that has been included in ActiveJob is integration with ActionMailer. Sending email is definitely a function that should be performed via a queue. Instead of using your selected library’s Mailer methods, use the two that ActiveJob natively supports deliver_now!
and deliver_later!
.
Like its name suggests, deliver_now!
executes the send right away. Use this instead of the deprecated deliver
method. The method which implements queues is deliver_later!
.
AccountMailer.welcome(Account.first).deliver_now!
AccountMailer.welcome(Account.first).deliver_later!
Need functionality like set
provides? Use these optional parameters!
AccountMailer.welcome(Account.first).deliver_later!(wait: 1.hour)
AccountMailer.welcome(Account.first).deliver_later!(wait_until: 10.hours.from_now)
Global ID
The last feature in the ActiveJob library is Global ID, which creates a URI to represent a model instance. This is helpful in situations relating to jobs. Instead of serializing a whole object or an id/class pair, just save the Global ID of the instance to the job.
At the time of job execution, the Global ID is used to locate the appropriate instance.
Let’s see this in action:
account = Account.first
account.global_id.to_s
=> "gid://sample/Account/1"
GlobalID::Locator.locate account.global_id
=> Account:0x007f9746c8a1b0
account.global_id.app
=> "sample"
account.global_id.model_name
=> "Account"
account.global_id.model_class
=> Account(id: integer, name: string, city: string, state: string, zipcode: string, latitude: integer, longitude: integer, created_at: datetime, updated_at: datetime)
account.global_id.model_id
=> "1"
Notice that a lot of data is stored in relation to the Global ID, allowing you to get a lot of meta data about the object instance.
The not so distant relative of Global ID is Signed Global ID, which is similar to Global ID except it is a signed object. The signed version can be accessed using the sgid
method on the object.
account.sgid
=> SignedGlobalID:0x007f97470cac98
account.sgid.to_s
=> "BAhJIhtnaWQ6Ly9zYW1wbGUvQWNjb3VudC8xBjoGRVQ=--4b86065bf01ecf72de68ea3d34d69f5241178ea1"
The SignedGlobalID
can be used to verify and retrieve a GlobalID
and includes the ability to set an expiration date for the signed global ID.
ActiveJob Compatible Libraries
As of the time this article was written, ActiveJob can support the following queuing libraries:
Wrapping Up
ActiveJob is a powerful and much needed addition to the Rails framework. Now that you understand the ActiveJob library, the ActionMailer counterpart, and Global ID, you can use ActiveJob to the fullest. I’d love to hear more about how you utilized ActiveJob for your application and the things you have learned so far. Please feel free to leave a comment. Let’s have a conversation!