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

  • Getting ‘rake test’ Running with Rails 3 and MongoDB

    I’ve recently started a re-write of my Addressbook application. Addressbook was my first Rails application, as well as my first full fledged Ruby application…and boy does it show! So, trust me, the re-write is justified :)

    I saw the re-write as a good opportunity to get more familiar with Rails 3 and MongoDB. After getting Rails 3 setup to use MongoDB via MongoMapper (Ben Scofield’s setup script gets you most of the way there), I quickly realized that the tests would not run.

    jwood-mbp:addressbook jwood$ rake test
    (in /Users/jwood/dev/personal/addressbook)
    Errors running test:units, test:functionals, test:integration!
    
    jwood-mbp:addressbook jwood$ rake test:units
    (in /Users/jwood/dev/personal/addressbook)
    rake aborted!
    Don't know how to build task 'db:test:prepare'
    

    Some Googling revealed this post by Yehuda Katz, in which he says:

    Additionally, users can completely remove the ActiveRecord gem, and be rid of the generators, rake tasks and other peripheral elements. An example: if DataMapper implements a db:test:prepare rake task, a Rails developer can replace ActiveRecord with DataMapper, and the test:units rake task will use DataMapper’s new tasks.

    We’re not using DataMapper here, but the same thing applies to MongoMapper. So, all that is needed is to implement a db:test:prepare task, which I did in lib/tasks/mongo.rake

    namespace :db do
      namespace :test do
        task :prepare do
          # Stub out for MongoDB
        end 
      end 
    end
    

    As of right now, the task does nothing. I may end up adding some functionality to it as I work on the project. We’ll see.

    Running the tests again, I can see that was all that was needed to get going.

    jwood-mbp:addressbook jwood$ rake test:units
    (in /Users/jwood/dev/personal/addressbook)
    Loaded suite /Users/jwood/.rvm/gems/ruby-1.9.1-p378@rails3/gems/
    rake-0.8.7/lib/rake/rake_test_loader
    Started
    ...
    Finished in 0.062854 seconds.
    
    3 tests, 3 assertions, 0 failures, 0 errors, 0 skips