The observer design pattern (also called pub-sub) allows one or more subscribers to register with and receive notifications from a publisher. It's great for scenarios that need push-based notifications, instead of continuous polling. A real-world example is this blog. Whenever a new post is published, it sends an email to all the subscribers of the blog. As a result, the readers don't have to constantly refresh the blog to see if a new post was published.
The pattern defines a publisher (also known as a provider or an observable) and zero or more subscribers (also known as observers or listeners). Subscribers register with the publisher, and whenever a predefined condition, event, or state change happens (e.g. a new post is published), the publisher automatically notifies all observers, passing any additional data to provide current state information to observers.
The Instrumentation (or Notification) API is a simple implementation of the observer (pub-sub) pattern in Rails. It allows you to subscribe and listen to various events that occur within your Rails application or even the Rails framework. In addition, you can also use it to benchmark a piece of code. This article explains how to use the instrumentation API and some practical examples of how Rails (the framework) uses it internally.
What we'll learn:
- How to publish an event
- How to subscribe to an event
- Publishing an event to multiple subscribers
- Subscribing to multiple events
- Performance benchmarking
- Example: Instrumenting Render
Events are a great way to decouple various aspects of your application. A single event can have multiple listeners in unrelated parts of the codebase, and a subscriber can listen to multiple events. The instrumentation API in Rails lets you create and publish such events to subscribers.
The Rails framework provides several of these events itself. For example,
- Active Record fires an event named
sql.active_record
every time it uses a SQL query on a database. You can subscribe to this event to keep track of the total number of queries made during an action. - Action controller fires an event
process_action.action_controller
after processing a controller action, allowing you to track how long it took to process that action.
How to Publish an Event
To publish an event that other parts of the code can listen to, call the ActiveSupport::Notifications.instrument
method, passing the event's name, payload (a hash containing information about the event), and an optional block. The payload is used to pass additional data to the event's subscribers.
ActiveSupport::Notifications.instrument "publish.post", { title: "hello world" } do
puts "Creating and publishing the post"
end
- If you pass a block, ActiveSupport will execute the block and then call all the event subscribers with the provided payload and the time taken to execute the block.
- If you don't pass the block, ActiveSupport will simply notify the subscribers.
Hence, the instrumentation API serves a dual purpose. You can use it to benchmark how long it took to execute some action, as well as to notify other parts of the codebase that a certain event occurred.
How to Subscribe to an Event
To listen to any custom events that you created, or the ones triggered by the Rails framework, call the ActiveSupport::Notifications.subscribe
method, providing the name of the event you're subscribing to and passing a block that will be called whenever this event occurs.
ActiveSupport::Notifications.subscribe "process_action.action_controller" do |name, started, finished, id, data|
name # => String, name of the event (such as 'render' from above)
start # => Time, when the instrumented block started execution
finish # => Time, when the instrumented block ended execution
id # => String, unique ID for the instrumenter that fired the
data # => Hash, the payload
end
Too many arguments? Receive an event instead...
If you don't want to type all those arguments each time you subscribe to an event, don't worry. Passing a block with a single argument will ensure it receives an ActiveSupport::Notifications::Event
object.
ActiveSupport::Notifications.subscribe "process_action.action_controller" do |event|
puts event.class # ActiveSupport::Notifications::Event
puts event.name # process_action.action_controller
puts event.duration # 0.03 (in ms)
puts event.payload
end
To continue our earlier example, you can subscribe to the event publish.post
using the following code:
ActiveSupport::Notifications.subscribe "publish.post" do |event|
puts "Published the post"
end
Publishing to Multiple Subscribers
Multiple subscribers can listen to a single event, allowing you to decouple your code. For example, you'd like to publish a newsletter as well as take a backup when you publish a post. You can accomplish this by dispatching an event after publishing a post.
# newsletter service
ActiveSupport::Notifications.subscribe "publish.post" do |event|
# send newsletter
end
# backup service
ActiveSupport::Notifications.subscribe "publish.post" do |event|
# take backup
end
# blogging-related code
ActiveSupport::Notifications.instrument "publish.post", { id: post.id } do
# publish the post
end
Subscribing to Multiple Events
Instead of the event name, you can pass a regular expression to the subscribe
method above. This lets you subscribe to multiple events at once.
Here's the code to subscribe to all the events from ActiveRecord
library.
ActiveSupport::Notifications.subscribe /active_record/ do |*args|
# inspect all ActiveRecord events
end
Alternatively, the subscriber can simply listen to each event separately.
Performance Benchmarking
Sometimes, you're only interested in measuring how long it took to execute a code block. The duration
property on the event object received by the subscriber returns the difference in milliseconds between when the execution of the event started and when it ended.
ActiveSupport::Notifications.subscribe "publish.post" do |event|
puts "#{event.duration} ms" # 3001.50 ms
end
ActiveSupport::Notifications.instrument "publish.post" do
sleep 3
puts "instrument: publishing the post"
end
This provides a simple way to figure out the parts of the codebase that are slowing the application down and optimize them.
Example: Instrumenting Render
Let's say you would like to measure how long it took to render an action. In your Rails controller, you'd wrap the call to the render
method in the instrument
block as follows:
ActiveSupport::Notifications.instrument('render', extra: :information) do
render 'index'
end
Rails will first execute the provided block by calling render
, and then notify all the subscribers.
You can listen to this event by registering a subscriber.
ActiveSupport::Notifications.subscribe('render') do |name, start, finish, id, payload|
# do something with the information provided.
end
This lets you measure how long it took to render the view as well as get notified whenever rendering happens.
That's a wrap. I hope you found this article helpful and you learned something new.
As always, if you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I reply to all emails I get from developers, and I look forward to hearing from you.
If you'd like to receive future articles directly in your email, please subscribe to my blog. If you're already a subscriber, thank you.