Rails Reloader: A Lesser Known Railtie Hook

I recently wrote a book about integrating with Rails from a Ruby gem, which specifically touched on using a Railtie to extend ActiveRecord, ActionController and ActionView . While these are the 3 more popular Rails libraries, there’s plenty others that are configurable.

A recent issue in Sucker Punch caused me to go digging through the Rails source code. Ultimately, the to_prepare method on ActionDispatch::Reloader resolved the issue, but I surprised was to find very little documentation about it.

The Problem

Sucker Punch lazily creates the Celluloid pools used for background job queues. For the purpose of keeping track of the queues already initialized, Sucker Punch makes use of the Celluloid Registry. Think of it as a class-level hash.

This works swimmingly in production, but not so much in development. Rails makes our lives easier by reloading code in between requests while in development, due to this setting in config/environments/development.rb:

  config.cache_classes = false

Without it, we’d be forced to restart the server after almost every request. If that sounds like a giant PITA to you, I whole heartedly agree!

So now you make your awesome job class, do some background work (send an email for example) and reload the page and boom:

Celluloid::PoolManager: async call `perform` aborted!
ArgumentError: A copy of SendInvitationJob has been removed from the module tree but is still active!
        gems/activesupport-4.0.3/lib/active_support/dependencies.rb:446:in `load_missing_constant'
        gems/activesupport-4.0.3/lib/active_support/dependencies.rb:184:in `const_missing'
        my-app/app/jobs/send_invitation_job.rb:6:in `block in perform'

The Celluloid registry still has reference to a the original SendInvitationJob class when it was initialized, however, reloading the code has caused the original reference to disappear and all hell breaks loose when the queue key is fetched to send another job to the class.

In my head, it made sense for the queues to be cleared out upon every request in development. In general, because Sucker Punch doesn’t have persistent queues, the best use case is for quick one-off jobs that aren’t extremely important — email and logging come to mind. Since both of these examples are typically pretty speedy, it’s unlikely there will be a huge job backup upon subsequent requests.

I knew what I wanted, but didn’t know how to accomplish it.

The Solution

Knowing the issue was related to the setting config.cache_classes = false in the development environment, I broke open the Rails source code and searched for cache_classes. The first result was the ActionDispatch reloader middleware. Fortunately, there’s a very descriptive comment at the top of the class:

  # By default, ActionDispatch::Reloader is included in the middleware stack
  # only in the development environment; specifically, when +config.cache_classes+
  # is false. Callbacks may be registered even when it is not included in the
  # middleware stack, but are executed only when <tt>ActionDispatch::Reloader.prepare!</tt>
  # or <tt>ActionDispatch::Reloader.cleanup!</tt> are called manually.

This functionality is exactly what I needed!. From here, I just needed to know what callbacks were valid. A few lines in to the class are the following methods:

# Add a prepare callback. Prepare callbacks are run before each request, prior
# to ActionDispatch::Callback's before callbacks.
def self.to_prepare(*args, &block)
  unless block_given?
    warn "to_prepare without a block is deprecated. Please use a block"
  end
  set_callback(:prepare, *args, &block)
end

# Add a cleanup callback. Cleanup callbacks are run after each request is
# complete (after #close is called on the response body).
def self.to_cleanup(*args, &block)
  unless block_given?
    warn "to_cleanup without a block is deprecated. Please use a block"
  end
  set_callback(:cleanup, *args, &block)
end

to_prepare and to_cleanup…and like the comments say, they do exactly what you’d expect. Given that I wanted to clear our the Celluloid registry BEFORE each request, on_prepare is the golden ticket. Now I just needed to figure out how to clear the registry.

A quick glade over the Celluloid::Registry class documentation shows some methods that might be of value. It turns out that these are instance methods for an instance of the Celluloid::Registry class. Unfortunately, when Celluloid boots, it instantiates a registry to use internally, so we need a way to get at that particular instance and clear it out. Sure enough, a class method to do just that in Celluloid::Actor is available.

Now that we all the pieces of the puzzle, it was time to put together a Railtie to trigger the behavior. Prior to needing this functionality, the Railtie in Sucker Punch was pretty simple:

module SuckerPunch
  class Railtie < ::Rails::Railtie
    initializer "sucker_punch.logger" do
      SuckerPunch.logger = Rails.logger
    end
  end
end

All it did was connect the logger to the existing Rails logger. Adding the callback to ActionDispatch looks like:

module SuckerPunch
  class Railtie < ::Rails::Railtie
    initializer "sucker_punch.logger" do
      SuckerPunch.logger = Rails.logger
    end

    config.to_prepare do
      Celluloid::Actor.clear_registry
    end
  end
end

Now when the Railtie is loaded, the Celluloid::Actor.clear_registry method is triggered before the reloading of code in the development environment, clearing out the Celluloid registry and allowing Sucker Punch to instantiate new job queues for each request.

Summary

I was unaware of any of these methods when the issue was submitted. Rather than throw my hands up and close the issue because it didn’t affect me, I thought through an approach that could work, and only then started to write code. And in fact, didn’t know what code to write!

Comments and well written code serve as great documentation. I probably wouldn’t have stumbled on ActionDispatch::Reloader without the detailed comments at the top of the class. Sure, I would’ve found the cache_classes line, but might not have given it more thought.

Next time you have a question about the syntax of a method or the order of its arguments, clone the repo (if it’s open source, of course) and do a search. I think you’ll be surprised at how quickly you can find what you’re looking for. My guess is you’ll also be pleasantly surprised at the other things you stumble upon in the process.

Comments