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