If you’re coming here for the first time, be sure to check two other articles I wrote on merb-cache
What is the dog pile effect?
Memcache is great when you want to cache some rather heavy queries. For example, one query I cache on a project looks like this:
select answers.question_id, answers.question_choice_id, COUNT(*) AS count, question_choices.text from answers INNER JOIN question_choices ON question_choice_id = question_choices.id WHERE answers.`question_id` = 1 AND answers.`media_id` = 1 GROUP BY answers.question_choice_id
It’s a rather long query when there are a lot of rows so it makes sense to cache it. Now, I don’t want to always serve the same data so I use the expiration feature of memcache (:expire_in in merb-cache).
But what happens when some huge traffic comes and your cache expires? If the data ir quick to generate it’s not a big problem but it it uses a rather long query and take a few seconds then while the thread that got the first cache miss will recalculate the data, other requests will get a cache miss and also recalculate the data. And this added calculation might slow down the whole system (or your db server) leading you to a death spiral…
Mint Store
To solve this problem, Glenn Franxman released MintCache a caching engine for Django http://www.djangosnippets.org/snippets/155/. The Disqus team then released another implementation of MintCache http://blog.disqus.net/2008/06/11/mintcache-simple-version/ Mint Store is a port of this work to merb-cache.
It solves the problem by adding the time by adding some metadata: the stale date and if the cache is currently being refreshed. When doing a read request, it checks if the stale date is passed and if it’s passed sets refreshed to true and returns nil.
It’s probably easier to understand looking at the code (I need to get better at explaining this kind of stuff)
def read(key, parameters = {})
packed_data = store_read(key, parameters)
return nil unless packed_data
data, refresh_time, refreshed = *packed_data
if !refreshed && (Time.now > refresh_time)
write(key, data, parameters, :expire_in => 0, :refreshed => true)
return nil
end
data
end
def get_metadata_and_normalize!(conditions = {})
expire_in = conditions.delete(:expire_in) || options[:expire_in]
refreshed = conditions.delete(:refreshed)
conditions[:expire_in] = expire_in + (conditions.delete(:mint_delay) || options[:mint_delay])
[Time.now + expire_in, refreshed]
end
Compared to Disqus’ MintCache, I added two features:
- When you use fetch (and provide a block), if the cache is stale, Mint Store will return the stale cache and update the cache (with the result of executing the provided block) after the request has been served using
Merb.run_later - Deletion will just mark the cache as stale which will cause the next fetch to repopulate the cache. (This can be disabled, see the initialization options)
Important Note: Read and Fetch
Read returns nil the first time the cache becomes stale and then returns the stale cache for :mint_delay seconds. So on the contrary to using fetch where none of the clients will be penalized, if you use read, you will penalize one clients who will have to wait for the cache to be refreshed before his request is served. (fetch_fragment and fetch_partial from merb-cache both use fetch)
Initialization Options
Mint Store accepts several initialization options:
-
Behaviour options:
:force_deleteif set to true, delete will just delete the data from the cache:need_expire_inif set to true, writable? will return false if the:expire_incondition is not present. If you are going to use MintStore with the AdHocStore it makes sense to set it.
-
Default values:
:mint_delay: the difference between the stale date (that you provide by:expire_in) and the real:expire_ingiven to memcached (default: 30s):refresh_delay: the:expire_invalue given to memcached while regenerating the cache (can set to 0 if you want memcache to never expire the stale cache while waiting for it to be refreshed):expire_in: default value for the stale date if not provided (default: 300s)
Example: setting the options
register(:memcached_store, Merb::Cache::MemcachedStore)
register(:mint_store, Merb::Cache::MintStore[:memcached_store], :need_expire_in => true, :refresh_delay => 0)
Where to get it
You can get it fresh from github at http://github.com/giom/mint_store Or install it as a gem with: gem install giom-mint_store --source http://gems.github.com
I’m currently available for hire on a contract basis.