ReloadablePath in Rails 3

A core feature of the Signal application is support for custom promotion web forms. Custom promotion web forms allow our customers to create custom web pages that will allow their customers to interact with their promotions via the web. Our customers currently use these web forms for sweepstakes entry, email/SMS subscription list opt-ins, online polls, and more.

One of the best things about custom promotion web forms is that it allows our customers to completely control what the web page looks like. The Signal application allows for the creation of a web form theme that can be used as a template for a given promotion. The specific promotion can then customize the web page further by specifying the copy that appears on the web page, the data attributes that should be collected, and more.

The web form themes are managed by the Signal application, and are saved to disk as a view (an ERB template) when created or updated. Our customers can edit these themes at any time. When a theme is updated, we need to tell Rails to clear the cache for these specific views, so our customer will see their changes the next time they visit a web page that uses the updated theme.

In Rails 2, this was done using ReloadablePath.

class SomeController < ApplicationController
  prepend_view_path
    ActionView::ReloadableTemplate::ReloadablePath.new(
      "/path/to/my/reloadable/views")
end

However, ReloadablePath is no more in Rails 3. So, we needed to find a new solution to this problem.

Rails 3 introduced the concept of a Resolver, which is responsible for finding, loading, and caching views. Rails 3 also comes with a FileSystemResolver that the framework uses to find and load view templates that are stored on the file system.

FileSystemResolver is very close to what we want. However, we need the ability to clear the view cache whenever one of the web form themes has been updated. Thankfully, this was fairly easy to do by creating a new Resolver that extends FileSystemResolver, which is capable of clearing the view cache if it determines that it needs to be cleared.

Looking at the code for the Resolver class, you can see that it checks the view cache in the find_all method. If it does not have the particular view cached, it will proceed to load it using the proper Resolver. So, we simply have to override find_all to clear the cache if necessary before delegating the work to the super class to find, load, and cache the view.

class ReloadablePathResolver < ActionView::FileSystemResolver

  def initialize
    super("/path/to/my/reloadable/views")
  end

  def find_all(*args)
    clear_cache_if_necessary
    super
  end

  def self.cache_key
    ActiveSupport::Cache.expand_cache_key("updated_at",
      "reloadable_templates")
  end

  private

  def clear_cache_if_necessary
    last_updated = Rails.cache.fetch(ReloadablePathResolver.cache_key) { Time.now }

    if @cache_last_updated.nil? || 
        @cache_last_updated < last_updated
      Rails.logger.info "Reloading reloadable templates"
      clear_cache
      @cache_last_updated = last_updated
    end
  end

end

Since we're running multiple processes in production, we need a way to signal all processes that their view caches should be cleared. So, we're using memcache to store the time that the web form themes were last updated. Each process then checks that timestamp against the time that particular process last updated its cache. If the timestamp in memcache is more recent, then the ReloadablePathResolver will clear the cache using the clear_cache method it inherited from Resolver.

Next, we need to add some code that will update memcache any time a web form theme has been updated and saved to disk.

class WebFormTheme < ActiveRecord::Base
  after_save :update_cache_timestamp

  private

  def update_cache_timestamp
    Rails.cache.write(ReloadablePathResolver.cache_key, Time.now)
  end
end

The final step is to simply prepend the view path with the new ReloadablePathResolver.

class SomeController < ApplicationController
  prepend_view_path ReloadablePathResolver.new
end

References

  • Be Sociable, Share!

      Leave a Reply

      Your email address will not be published. Required fields are marked *