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:
- http://github.com/wycats/merb/tree/1.0.x/merb-cache/README Nice short tutorial (and more uptodate)
- http://merbunity.com/tutorials/15 Goes into more details on the ideas that guided merb-cache’s design (the examples are the same as above)
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
| Name | Context | Usage | Usable stores | Key type |
|---|---|---|---|---|
cache | controller | caches the result of the action | PageStore, ActionStore | controller |
eager_cache | controller | recaches the result of another action after the current action finishes using run later (great against dog piling) | PageStore, ActionStore | controller |
fetch_partial | view | fetches or caches the result of a partial | GzipStore, Sha1Store, FileStore, MemcacheStore | string (template_location returned by _template_for) |
fetch_fragment | controller or view | fetches or caches the result of a proc | GzipStore, Sha1Store, FileStore, MemcacheStore | string either :cache_key or the file and line the proc it’s called with is declared |
Strategic Stores
| Store | Key Type | writable? condition | Usage | Remarks |
|---|---|---|---|---|
| AdHocStore | any | looks for its first writable? store | Selects the most appropriate store | order is important (should be from least likely to most likely writable) |
| PageStore | controller | get request with no query string parameters (e.g. ?q=z) and no :if or :unless condition | cache the page for webserver | no support for reading (it’s the work of the webserver) |
| ActionStore | controller | only checks its stores | cache actions that have parameters or conditions for caching | doesn’t encode content_type in it’s key |
| GzipStore | any | always true | compresses cache with gzip | Useful with MemcachedGzip |
| Sha1Store | string, numeric or symbol | only checks its stores | digest key + params with sha1 |
Fundamental stores
| Store | Key Type | writable? condition | Usage |
|---|---|---|---|
| FileStore | string, numeric or symbol | no :expire_in condition | Store the cache on the file system. |
| MemcacheStore | any | always true | Store cache in memcache with expiration (:expire_in condition) |
I’m currently available for hire on a contract basis.