A few days ago the new rails 4.2 beta release was announced and the usual bag of goodies is going to make our life as developers easier and more interesting.
The most relevant new feature is the Active Job framework and its integration with ActionMailer: rails has now its own queue interface and you can swap the queuing gem (resque, delayed job, sidekiq…) without changing your application code.
You can even send email asyncronously with the new deliver_later
method, while if you want sync delivery you should use instead deliver_now
because the old deliver
method is now deprecated.
If you don’t add any queue gem to the Gemfile then the default rails system will be used, which means that everything will be sent immediately (no async functionality).
This brilliant feature depends on a couple of new gems: active job and global id. Active Job usage is well documented in the new official guide, so I will focus most of this article on Global Id.
If you want to follow along you can download a demo app from this repository, bundle the gems and start the server with rails s
. The example is basically a rails 4.2 app with a regular ActiveRecord Friend scaffold and a NameCapitalizerJob job class.
How do the new gems interact exactly? Let’s enqueue a new job for the NameCapitalizerJob worker, which is located in the example application:
NameCapitalizerJob.perform_later Friend.first
=> #<NameCapitalizerJob:0x007fb44acaff48 ...>
If you used rails queuing systems in the past you already know that it was necessary to pass your ActiveRecord objects to the worker in the form of their id and manually reload the record at job execution. This is no more required, so in the example above we’re simply passing the record itself, while in the job code the reload is automatic:
NameCapitalizerJob < ActiveJob::Base
def perform(friend)
name = friend.name.capitalize
friend.update_attribute :name, name
end
end
You can see the job has been correctly enqueued:
Delayed::Job.count
=> 1
with the following params:
YAML.load(Delayed::Job.first.handler).args
=> [NameCapitalizerJob, "4a33725b-35cf-4940-b1ca-d6fad84d410f", "gid://activejob-example/Friend/1"]
These params represent: the job class name, the job id, and the global id string representation for the Friend.first
record.
The format of the global id string is gid://AppName/ModelClassName/record_id
.
Given a gobal id, the worker will load the referenced record transparently. This is achieved by mixing into ActiveRecord::Base a module from the global id gem:
puts ActiveRecord::Base.ancestors
...
GlobalID::Identification
...
The GlobalID::Identification
module defines only a couple of methods: #global_id
, #signed_global_id
and their aliases #gid
and #sgid
, where the first is the record’s globalid object, the second is the record’s signed(encrypted) version of the same object:
gid = Friend.first.gid
=> #<GlobalID:0x007fa9add041f8 ...>
gid.app
=> "activejob-example"
gid.model_name
=> "Friend"
gid.model_class
=> Friend(id: integer, name: string, email: string...)
gid.model_id
=> "1"
gid.to_s
=> "gid://activejob-example/Friend/1"
sgid = Friend.first.sgid
=> #<SignedGlobalID:0x007fa9add15e58 ...>
sgid.to_s
=> "BAh7CEkiCGdpZAY6BkVUSSIlZ2lkOi8vYWN0aXZl..."
The most important thing here is the string representation of the gid, as it contains enough information to retrieve its original record:
GlobalID::Locator.locate "gid://activejob-example/Friend/1"
=> #<Friend id: 1, name: "John smith" ...>
The actual source code used for locating records is rather simple and self explanatory:
class ActiveRecordFinder
def locate(gid)
gid.model_class.find gid.model_id
end
end
Regarding the signed object, we can inspect the original data hidden into its string representation using the following code:
SignedGlobalID.verifier.verify sgid.to_s
=> {"gid"=>"gid://activejob-example/Friend/1", "purpose"=>"default", "expires_at"=>Mon, 29 Sep 2014 08:25:31 UTC +00:00}
That’s how the global id gem works inside rails. By the way, it’s rather easy to use it with your own classes. Let’s see how to do it.
Let’s start a new irb session with bundle exec irb
from the app folder, and require it with require 'globalid'
.
The needs an app
namespace in order to generate ids, so we need to set it manually:
GlobalID.app = 'TestApp'
Now let’s build a PORO class that defines globalid required methods (::find
and id
) and includes the GlobalID::Identification
module:
class Item
include GlobalID::Identification
@all = []
def self.all; @all end
def self.find(id)
all.detect {|item| item.id.to_s == id.to_s }
end
def initialize
self.class.all << self
end
def id
object_id
end
end
As you might guess, the ::find
method retrieves an item from its id code, while the #id
method is simply an identifier. It works like this:
item = Item.new
=> #<Item:0x007fdb4b05da10>
id = item.id
=> 70289916620040
Item.find(id)
=> #<Item:0x007fdb4b05da10>
Time to get the item global id:
gid = item.gid
=> #<GlobalID:0x007fdb4b026358 ...>
gid.app
=> "TestApp"
gid.model_name
=> "Item"
gid.model_id
=> "70289916620040"
gid_uri = gid.to_s
=> "gid://TestApp/Item/70289916620040"
We can now retrieve the original Item object from the gid_uri:
found = GlobalID.locate gid_uri
=> #<Item:0x007fdb4b05da10>
found == item
=> true
That’s it! I encourage you to check out the global id gem code on github, it’s just a few files so it’s very readable.
Leave a Reply