Created
June 25, 2012 15:18
-
-
Save cmsd2/2989216 to your computer and use it in GitHub Desktop.
preventing cache stampede / dogpiling on rails
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class FixedWindowCacheRefreshPolicy | |
def initialize(window) | |
@window = window | |
end | |
def nearly_expired?(cache_entry) | |
now = Time.now | |
expires_at = cache_entry.expires_at | |
if expires_at and (now + @window) > expires_at | |
Rails.logger.info "entry nearly expired. now: #{now} expires_at: #{expires_at}" | |
true | |
else | |
Rails.logger.info "entry still good. now: #{now} expires_at: #{expires_at}" | |
false | |
end | |
end | |
end | |
module CacheStampedePrevention | |
def self.included(base) | |
base.extend(ClassMethods) | |
base.around_filter :prevent_cache_stampede | |
end | |
def prevent_cache_stampede | |
begin | |
Rails.logger.info "using cache-stampede prevention. cache_configured? #{cache_configured?}" | |
@locked_paths = [] | |
yield | |
ensure | |
@locked_paths.each do |key_options| | |
key, options = key_options | |
unlock_fragment(key, options) | |
end | |
end | |
end | |
def default_cache_refresh_policy | |
FixedWindowCacheRefreshPolicy.new(1.minute) | |
end | |
class CacheEntry | |
attr_accessor :expires_at | |
attr_accessor :value | |
def initialize(value, expires_at = nil) | |
@value = value | |
@expires_at = expires_at | |
end | |
end | |
def html_safe_value(obj) | |
obj.respond_to?(:html_safe) ? obj.html_safe.to_s : obj | |
end | |
def lookup_refresh_policy(*args) | |
klass = args.shift | |
return unless klass | |
if klass.is_a?(Symbol) | |
klass = klass.to_s | |
end | |
if klass.is_a?(String) | |
klass = klass.classify.constantize | |
end | |
if klass.is_a?(Class) | |
klass = klass.new(*args) | |
end | |
klass | |
end | |
def get_refresh_policy(options) | |
args = options[:refresh_policy] || [] | |
lookup_refresh_policy(*args) || default_cache_refresh_policy | |
end | |
def read_fragment(key, options = nil) | |
return unless cache_configured? | |
key = fragment_cache_key(key) | |
instrument_fragment_cache :read_fragment, key do | |
result = cache_store.read(key, options) | |
if result.is_a?(CacheEntry) | |
policy = get_refresh_policy(options) | |
if policy.nearly_expired?(result) && lock_fragment(key, options) | |
result = nil | |
else | |
result = result.value | |
end | |
end | |
html_safe_value(result) | |
end | |
end | |
def write_fragment(key, content, options = nil) | |
return content unless cache_configured? | |
key = fragment_cache_key(key) | |
instrument_fragment_cache :write_fragment, key do | |
content = html_safe_value(content) | |
expires_at = options[:expires_in] ? (Time.now + options[:expires_in]) : nil | |
entry = CacheEntry.new(content, expires_at) | |
cache_store.write(key, entry, options) | |
end | |
content | |
end | |
def lock_fragment(key, options) | |
lock_key = lock_cache_key(key) | |
options = options.merge(:unless_exist => true, :expires_in => 1.minute) | |
if cache_store.write(lock_key, true, options) | |
Rails.logger.info "locked #{key}" | |
@locked_paths << [key, options] | |
true | |
else | |
Rails.logger.info "didn't get lock on #{key}" | |
false | |
end | |
end | |
def lock_cache_key(key) | |
key + "-lock" | |
end | |
def unlock_fragment(key, options) | |
lock_key = lock_cache_key(key) | |
Rails.logger.info "unlocking #{key}" | |
cache_store.delete(lock_key, options) | |
end | |
module ClassMethods | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment