Merb-cache and its stores

Posted by Guillaume Maury
on Dec 09, 08

I’ve been playing around quite a lot with merb-cache so I thought I would write a bit about merb-cache to help others. I haven’t written many tutorials before so I might err too much on the side of over-explaining things. So please, tell me if you liked this article… and if you see any fault in my english, I’d be very glad to learn about them (3 years in japan pretty much destroyed my english).

For a basic introduction to merb-cache, you should read:

You might also want to first read another article in the serie Merb-cache’s methods

In this first part, I’ll describe the different stores available, in which situations they kick in and important remarks about there usage. I’ll use a top down approach and start with the strategy stores.

There is a quick summary/cheat sheet at the end…

The Strategy Stores

The AdHocStore

Used to select the most appropriate store.

Conditions for usage (writable?)

If you look at its writable? method

    def writable?(key, parameters = {}, conditions = {})
      @stores.capture_first {|s| s.writable?(key, parameters, conditions)}
    end

It looks for the first store in the list that is writable?

Writing

Same thing, for the write method:

    def write(key, data = nil, parameters = {}, conditions = {})
      @stores.capture_first {|s| s.write(key, data, parameters, conditions)}
    end
Important note

Notice that the order is important, when setting the Adhoc store up the stores you give should be given in the order of the least likely to the most likely to work (meaning writable? true)

	register(:correct, Merb::Cache::AdhocStore[:page_store, :action_store, :fragment_store])
	register(:wrong,   Merb::Cache::AdhocStore[:action_store, :page_store, :fragment_store])

if you use the :wrong adhoc store, the page store will never be triggered.

The PageStore:

Used to cache the result of a complete page so as to use it directly in nginx (or other server you might have) without hitting the merb application at all (much faster).

Conditions for usage (writable?)

Same here, let’s look at its writable? method

  def writable?(dispatch, parameters = {}, conditions = {})
      if Merb::Controller === dispatch && dispatch.request.method == :get &&
          !dispatch.request.uri.nil? && !dispatch.request.uri.empty? &&
          !conditions.has_key?(:if) && !conditions.has_key?(:unless) &&
          query_string_present?(dispatch)
        @stores.any? {|s| s.writable?(normalize(dispatch), parameters, conditions)}
      else
        false
      end
    end

    def query_string_present?(dispatch)
      dispatch.request.env["REQUEST_URI"] == dispatch.request.uri
    end

It only accepts controllers that received a get request that have no query string parameters (e.g. ?q=z) and doesn’t cache any actions that has a if or unless conditions (e.g. cache :show, :unless => :logged_in?)

Writing

    def normalize(dispatch)
      key = dispatch.request.uri.split('?').first
      key << "index" if key =~ /\/$/
      key << ".#{dispatch.content_type}" unless key =~ /\.\w{2,6}/
      key
    end

	def write(dispatch, data = nil, parameters = {}, conditions = {})
      if writable?(dispatch, parameters, conditions)
        @stores.capture_first {|s| s.write(normalize(dispatch), data || dispatch.body, {}, conditions)}
      end
    end

The normalize method that is used to generate a key, creates a key depending on the uri and content_type. The write method doesn’t send any parameters (which would be encoded as part of the key by memcached_store and file_store), so the page cache key doesn’t depend on any parameters not encoded in the uri (consistent with writable? policy of only returning true when there are no query string parameters)

Important note

PageStore doesn’t support reading as it’s intended for use with a server like nginx (I’ll give some examples of nginx configuration in a later blog post). So read always returns nil.

  def read(dispatch, parameters = {})
    nil
  end

The ActionStore

Used to cache actions that have parameters or conditions for caching. Because the request will have to hit the merb application (and go through routing), it’s much slower than page store caching.

Conditions for usage (writable?)

    def writable?(dispatch, parameters = {}, conditions = {})
      case dispatch
      when Merb::Controller
        @stores.any?{|s| s.writable?(normalize(dispatch), parameters, conditions)}
      else false
      end
    end

As long as dispatch is a controller (meaning that the store is used by the cache controller method, e.g. cache :show) and of course that it has a substore for which store.writable?(normalize(dispatch), parameters, conditions) return true (but this is common to all strategic stores), it will be writable.

Writing

    def write(dispatch, data = nil, parameters = {}, conditions = {})
      if writable?(dispatch, parameters, conditions)
        @stores.capture_first {|s| s.write(normalize(dispatch), data || dispatch.body, parameters, conditions)}
      end
    end

   def normalize(dispatch)
      "#{dispatch.class.name}##{dispatch.action_name}" unless dispatch.class.name.blank? || dispatch.action_name.blank?
    end

The key only depends on the controller and action but the action store also passes along its parameters and conditions meaning that they too will be encoded in the key down the road.

Important note

The action store doesn’t take into account the content_type when caching, so if you use it for caching an action that provides more than one content_type, you will be in for some nasty surprise. (more on that in another blog post)

The GzipStore

Compresses the content of the cache with gzip. Useful with a nginx module like MemcachedGzip that serves the gzip content directly from memcached (and decompresses it on the fly for clients that do not support gzip content)

writable? always return true and when writing it keeps the key, parameters and conditions and just compresses the data.

The Sha1Store

It uses sha1 to digest the keys.

    def write(key, data = nil, parameters = {}, conditions = {})
      if writable?(key, parameters, conditions)
        @stores.capture_first {|c| c.write(digest(key, parameters), data, {}, conditions)}
      end
    end

    def digest(key, parameters = {})
      @map[[key, parameters]] ||= Digest::SHA1.hexdigest("#{key}#{parameters.to_sha2}")
    end

One thing to note is that it memoizes the result of the sha1 digest for the whole time your application is running.

Fundamental Stores

Filestore

Used to store the cache on the file system.

Conditions for usage (writable?)

    # File caching does not support expiration time.
    def writable?(key, parameters = {}, conditions = {})
      case key
      when String, Numeric, Symbol
        !conditions.has_key?(:expire_in)
      else nil
      end
    end

Works as long as the key is a string, numeric or symbol and when there is no :expire_in condition (memcached is better for that) So:

  • Doesn’t work when called directly from the controller cache class method (it needs a strategy store like ActionStore or PageStore to generate the key for it and give it the data)
  • Works with fetch_partial

MemcacheStore

Used to store cache in memcache

Conditions for usage (writable?)

Memcached store consideres all keys and parameters writable so writable? is always true

Writing

    # Returns cache key calculated from base key
    # and SHA2 hex from parameters.
    def normalize(key, parameters = {})
      parameters.empty? ? "#{key}" : "#{key}--#{parameters.to_sha2}"
    end

    # Writes data to the cache using key, parameters and conditions.
    def write(key, data = nil, parameters = {}, conditions = {})
      if writable?(key, parameters, conditions)
        begin
          @memcached.set(normalize(key, parameters), data, expire_time(conditions))
          true
        rescue
          nil
        end
      end
    end

The key is normalized by adding the parameters sha2 digest. When writing the expiration time is set to Time.now + :expire_in or 0

Summary

Cache Methods

NameContextUsageUsable storesKey type
cachecontrollercaches the result of the actionPageStore, ActionStorecontroller
eager_cachecontrollerrecaches the result of another action after the current action finishes using run later (great against dog piling)PageStore, ActionStorecontroller
fetch_partialviewfetches or caches the result of a partialGzipStore, Sha1Store, FileStore, MemcacheStorestring (template_location returned by _template_for)
fetch_fragmentcontroller or viewfetches or caches the result of a procGzipStore, Sha1Store, FileStore, MemcacheStorestring either :cache_key or the file and line the proc it’s called with is declared

Strategic Stores

StoreKey Typewritable? conditionUsageRemarks
AdHocStoreanylooks for its first writable? storeSelects the most appropriate storeorder is important (should be from least likely to most likely writable)
PageStorecontrollerget request with no query string parameters (e.g. ?q=z) and no :if or :unless conditioncache the page for webserverno support for reading (it’s the work of the webserver)
ActionStorecontrolleronly checks its storescache actions that have parameters or conditions for cachingdoesn’t encode content_type in it’s key
GzipStoreanyalways truecompresses cache with gzipUseful with MemcachedGzip
Sha1Storestring, numeric or symbolonly checks its storesdigest key + params with sha1

Fundamental stores

StoreKey Typewritable? conditionUsage
FileStorestring, numeric or symbolno :expire_in conditionStore the cache on the file system.
MemcacheStoreanyalways trueStore cache in memcache with expiration (:expire_in condition)

I’m currently available for hire on a contract basis.

blog comments powered by Disqus