Skip to content

Instantly share code, notes, and snippets.

@rorcraft
Last active December 23, 2015 07:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rorcraft/6598302 to your computer and use it in GitHub Desktop.
Save rorcraft/6598302 to your computer and use it in GitHub Desktop.
dalli race_condition_ttl

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]
        

https://github.com/rails/rails/blob/3182295ce2fa01b02cb9af0b977a9cf83cc5d9aa/activesupport/lib/active_support/cache.rb#L287

      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

https://github.com/rails/rails/blob/3182295ce2fa01b02cb9af0b977a9cf83cc5d9aa/activesupport/lib/active_support/cache.rb#L535

        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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment