11 2 / 2016
Using ActiveSupport::Notifications is harder than it should be
ActiveSupport::Notifications are an incredibly useful subsystem in Ruby on Rails. They are used for everything from logging to database instrumentation but are second class citizens when it comes to actually developing with for several reasons.
Constants are reloaded but subscribers are instances
Rails has a very handy way of reloading constants when in the development mode that allows the developer to change code and have it reload on every page load. The problem is that subscribers are not instantiated on every page load, they are instantiated once. Even if you changed your subscriber code, it wouldn’t change the actual subscription, it would still be using the old instance.
Constants Are Lazy-Loaded By Default
When developing, Rails does not actually load every constant unless you’ve explicitly turned on eager_loading. This means that even if you had a collection of Subscriber objects, you wouldn’t be able to set them up unless you refer to them explicitly somewhere, which only happens when actually creating the subscription …
The Solution
To get around these problems and make it easier to use notifications to their full potential, this is the pattern that I use:
- Make a directory for the (now easily testable!) Subscribers
mkdir app/subscribers
- Create abstract subscriber (this is a bit ugly because there are currently no public methods to get to manage subscribers). In
app/subscribers/reloadable_subscriber.rb:class ReloadableSubscriber def self.subscribe_to(pattern) # Delete the old subscription if there is one ActiveSupport::Notifications.notifier.instance_variable_get(:@subscribers).each do |subscriber| if delegate = subscriber.instance_variable_get(:@delegate) # Class comparison is invalid, they are reloaded. ActiveSupport::Notifications.unsubscribe(subscriber) if self.to_s == delegate.class.to_s end end # Create a new subscription ActiveSupport::Notifications.subscribe(pattern, new) end end - Refer to constants manually if eager_load is set to false (the default). In
config/environments/development.rb# manually refer to the constants in the subscribers directory config.to_prepare do Dir.glob(Rails.root.to_s + '/app/subscribers/**/*_subscriber.rb').each { |file| File.basename(file, '.rb').camelize.constantize } end - Create a subscriber in
/app/subscribers/test_subscriber.rbclass TestSubscriber < ReloadableSubscriber subscribe_to 'test_notification' def call(name, started, finished, unique_id, payload) Rails.logger.debug "Test subscriber notified!" end end
Results
irb(main):001:0> ActiveSupport::Notifications.instrument('test_notification')
Test subscriber notified!
=> nil
irb(main):002:0> reload! # after code changed
Reloading...
=> true
irb(main):003:0> ActiveSupport::Notifications.instrument('test_notification')
Test subscriber notified with a change!