https://github.com/mperham/dalli/blob/v1.1.4/lib/active_support/cache/dalli_store.rb#L41
def initialize
...
# Extend expiry by stale TTL or else memcached will never return stale data.
# See ActiveSupport::Cache#fetch.
options[:expires_in] += options[:race_condition_ttl] if options[:expires_in] && options[:race_condition_ttl]
def fetch(name, options = nil)
if block_given?
options = merged_options(options)
key = namespaced_key(name, options)
cached_entry = find_cached_entry(key, name, options) unless options[:force]
entry = handle_expired_entry(cached_entry, key, options)
if entry
get_entry_value(entry, name, options)
else
save_block_result_to_cache(name, options) { |_name| yield _name }
end
else
read(name, options)
end
end
def handle_expired_entry(entry, key, options)
if entry && entry.expired?
race_ttl = options[:race_condition_ttl].to_i
if race_ttl && (Time.now.to_f - entry.expires_at <= race_ttl)
# When an entry has :race_condition_ttl defined, put the stale entry back into the cache
# for a brief period while the entry is begin recalculated.
entry.expires_at = Time.now + race_ttl
write_entry(key, entry, :expires_in => race_ttl * 2)
else
delete_entry(key, options)
end
entry = nil
end
entry
end
https://github.com/mperham/dalli/blob/v1.1.4/lib/active_support/cache/dalli_store.rb#L133
# Write an entry to the cache.
def write_entry(key, entry, options) # :nodoc:
method = options[:unless_exist] ? :add : :set
value = options[:raw] ? entry.value.to_s : entry
expires_in = options[:expires_in].to_i
if expires_in > 0 && !options[:raw]
# Set the memcache expire a few minutes in the future to support race condition ttls on read
expires_in += 5.minutes
end
@data.send(method, escape_key(key), value, expires_in, options)
rescue Dalli::DalliError => e
logger.error("DalliError: #{e.message}") if logger
false
end
- Memcache is set to expire in 5 min + options[:expire_in], :expire_in is stored as attribute and serialized.
#<ActiveSupport::Cache::Entry:0x007ff3333be290 @compressed=false, @expires_in=10.0, @created_at=1379443528.745184, @value="\x04\bI\"\rmy_value\x06:\x06ET">
- Rails will always read the entry as long as within the expire_in + 5 min window.
- It is not expired when Time.now is within serialized expires_in + created_at.
- return read value
- If Time.now is within serialized expires_in + race_condition_ttl + created_at
- It'll set expire in 2 * race_condition_ttl and memcache expire in 5 min + 2 * race_condition_ttl with stale value.
- Other clients will read stale value, with the updated expire_in
- It'll continue to update the key yielding the block
- (what if it fails to update the new value? - continue to read the stale value until 2 * race_condition)
- If Time.now > expires_in + race_condition_ttl + created_at
- It is expired, Rails will delete the key and yield block.
- Race condition still exists here.
- If Time.now > 5 min + options[:expire_in], memcache would have deleted the key.
- It will just yield the block to set key, value.
- Race condition still exists here.