Cache watch
Web frameworks like Merb or Rails provide convenient ways for caching output data to static files or other stores, used for improving a web application's performance. Caching is typically handled inside controller classes. With merb-cache, for example, we can cache an entire page by doing something along the lines of:
class Foo < Merb::Controller cache_page :index end
Expiring cached data is handled with a number of instance methods available to controllers, such as
expire_page(key)
or
expire_all_pages
.
This implies that cache expiration needs to be put explicitly in place inside actions.
The most common event signifying the need for cache expiration is the modification of the underlying data which has at some point been cached. More often than not, this means some sort of write (insert, update, delete) storage operation, which in turn means that cache expiration is closer to storage aware parts of the application rather than controllers. With this in mind, it would be useful to be able to configure cache expiration in a manner similar to that of cache creation, for example:
class Foo < Merb::Controller cache_page :index cache_watch :foo_store, :bar_store end
The
cache_watch :foo_store, :bar_store
line signifies that any cached artifacts associated with this controller need to be expired whenever a data altering operation takes place in the context of the
FooStore
or
BarStore
classes.
Approaching data altering operations as events presents a good case for employing the Observer pattern in order to enable cache expiration when such events take place. ActiveRecord, for instance, offers means for adding hooks to persistent objects' life cycle methods in the form of Observers.
class FooObserver < ActiveRecord::Observer def after_save(foo) expire_cache end end
Putting it all together, we can create a module that enables configuring cache expiration declaratively inside controllers in a way reminiscent to how cache creation is handled.
module CacheInvalidator def cache_watch(controller, *models) models.each {|model| (@entries ||= Set.new) << Entry.new(controller, model)} end def activate! @entries.each do |entry| return nil if Kernel.const_defined?(entry.class_name) entry.log observer = Class.new(ActiveRecord::Observer) do include CacheInvalidator observe(entry.model) define_method(:entry) {entry} end Kernel.const_set(entry.class_name, observer) observer.instance end end module_function :watch module_function :activate! def after_save(model) destroy_cache end def after_destroy(model) destroy_cache end private def destroy_cache FileUtils.rm_f(entry.file_path) if File.file?(entry.file_path) FileUtils.rm_r(entry.dir_path) if File.directory?(entry.dir_path) end class Entry attr_reader :controller, :model def initialize(controller, model) @controller, @model = controller, model end def class_name (controller.name.gsub(/\:\:/, '') + model.to_s.camelize + "CacheObserver").intern end def ==(other) controller == other.controller && self.model == other.model end def file_path "#{dir_path}.xml" end def dir_path "#{APP_ROOT}/public/cache/#{@controller.name.underscore}" end def log logger.info "Cache-watching #{model.to_s.camelize} for #{controller}" end end end
By including the
CacheInvalidator
module we can declare cache invalidation rules inside controllers.
class FooController < Merb::Controller include CacheInvalidator cache_page :index cache_watch :FooStore, :BarStore end
The cache can be activated where app initialization tasks are kept, such as
init.rb
in Merb.
Merb::BootLoader.after_app_loads do CacheInvalidator.activate! end