Skip to content

Instantly share code, notes, and snippets.

@cmsd2
Created June 25, 2012 15:18
Show Gist options
  • Save cmsd2/2989216 to your computer and use it in GitHub Desktop.
Save cmsd2/2989216 to your computer and use it in GitHub Desktop.
preventing cache stampede / dogpiling on rails
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