class ActiveSupport::Cache::MemCacheStore

Memcached Cache Store

A cache store implementation which stores data in Memcached: memcached.org

This is currently the most popular cache store for production websites.

Special features:

MemCacheStore implements the Strategy::LocalCache strategy which implements an in-memory cache inside of a block.

Constants

ESCAPE_KEY_CHARS
KEY_MAX_SIZE
OVERRIDDEN_OPTIONS

These options represent behavior overridden by this implementation and should not be allowed to get down to the Dalli client

Public Class Methods

new(*addresses) click to toggle source

Creates a new MemCacheStore object, with the given memcached server addresses. Each address is either a host name, or a host-with-port string in the form of “host_name:port”. For example:

ActiveSupport::Cache::MemCacheStore.new("localhost", "server-downstairs.localnetwork:8229")

If no addresses are provided, but ENV['MEMCACHE_SERVERS'] is defined, it will be used instead. Otherwise, MemCacheStore will connect to localhost:11211 (the default memcached port).

Calls superclass method ActiveSupport::Cache::Store::new
# File lib/active_support/cache/mem_cache_store.rb, line 77
def initialize(*addresses)
  addresses = addresses.flatten
  options = addresses.extract_options!
  if options.key?(:cache_nils)
    options[:skip_nil] = !options.delete(:cache_nils)
  end
  super(options)

  unless [String, Dalli::Client, NilClass].include?(addresses.first.class)
    raise ArgumentError, "First argument must be an empty array, address, or array of addresses."
  end

  @mem_cache_options = options.dup
  # The value "compress: false" prevents duplicate compression within Dalli.
  @mem_cache_options[:compress] = false
  (OVERRIDDEN_OPTIONS - %i(compress)).each { |name| @mem_cache_options.delete(name) }
  @data = self.class.build_mem_cache(*(addresses + [@mem_cache_options]))
end
supports_cache_versioning?() click to toggle source

Advertise cache versioning support.

# File lib/active_support/cache/mem_cache_store.rb, line 38
def self.supports_cache_versioning?
  true
end

Public Instance Methods

clear(options = nil) click to toggle source

Clear the entire cache on all memcached servers. This method should be used with care when shared cache is being used.

# File lib/active_support/cache/mem_cache_store.rb, line 171
def clear(options = nil)
  rescue_error_with(nil) { @data.with { |c| c.flush_all } }
end
decrement(name, amount = 1, options = nil) click to toggle source

Decrement a cached integer value using the memcached decr atomic operator. Returns the updated value.

If the key is unset or has expired, it will be set to 0. Memcached does not support negative counters.

cache.decrement("foo") # => 0

To set a specific value, call write passing raw: true:

cache.write("baz", 5, raw: true)
cache.decrement("baz") # => 4

Decrementing a non-numeric value, or a value written without raw: true, will fail and return nil.

# File lib/active_support/cache/mem_cache_store.rb, line 158
def decrement(name, amount = 1, options = nil)
  options = merged_options(options)
  key = normalize_key(name, options)

  instrument(:decrement, key, amount: amount) do
    rescue_error_with nil do
      @data.with { |c| c.decr(key, amount, options[:expires_in], 0) }
    end
  end
end
increment(name, amount = 1, options = nil) click to toggle source

Increment a cached integer value using the memcached incr atomic operator. Returns the updated value.

If the key is unset or has expired, it will be set to amount:

cache.increment("foo") # => 1
cache.increment("bar", 100) # => 100

To set a specific value, call write passing raw: true:

cache.write("baz", 5, raw: true)
cache.increment("baz") # => 6

Incrementing a non-numeric value, or a value written without raw: true, will fail and return nil.

# File lib/active_support/cache/mem_cache_store.rb, line 132
def increment(name, amount = 1, options = nil)
  options = merged_options(options)
  key = normalize_key(name, options)

  instrument(:increment, key, amount: amount) do
    rescue_error_with nil do
      @data.with { |c| c.incr(key, amount, options[:expires_in], amount) }
    end
  end
end
inspect() click to toggle source
# File lib/active_support/cache/mem_cache_store.rb, line 96
def inspect
  instance = @data || @mem_cache_options
  "#<#{self.class} options=#{options.inspect} mem_cache=#{instance.inspect}>"
end
stats() click to toggle source

Get the statistics from the memcached servers.

# File lib/active_support/cache/mem_cache_store.rb, line 176
def stats
  @data.with { |c| c.stats }
end
write(name, value, options = nil) click to toggle source

Behaves the same as ActiveSupport::Cache::Store#write, but supports additional options specific to memcached.

Additional Options

  • raw: true - Sends the value directly to the server as raw bytes. The value must be a string or number. You can use memcached direct operations like increment and decrement only on raw values.

  • unless_exist: true - Prevents overwriting an existing cache entry.

# File lib/active_support/cache/mem_cache_store.rb, line 102
      

Private Instance Methods

delete_entry(key, **options) click to toggle source

Delete an entry from the cache.

# File lib/active_support/cache/mem_cache_store.rb, line 238
def delete_entry(key, **options)
  rescue_error_with(false) { @data.with { |c| c.delete(key) } }
end
deserialize_entry(payload, raw: false, **) click to toggle source
# File lib/active_support/cache/mem_cache_store.rb, line 269
def deserialize_entry(payload, raw: false, **)
  if payload && raw
    Entry.new(payload)
  else
    super(payload)
  end
end
normalize_key(key, options) click to toggle source

Memcache keys are binaries. So we need to force their encoding to binary before applying the regular expression to ensure we are escaping all characters properly.

# File lib/active_support/cache/mem_cache_store.rb, line 253
def normalize_key(key, options)
  key = super
  if key
    key = key.dup.force_encoding(Encoding::ASCII_8BIT)
    key = key.gsub(ESCAPE_KEY_CHARS) { |match| "%#{match.getbyte(0).to_s(16).upcase}" }

    if key.size > KEY_MAX_SIZE
      key_separator = ":hash:"
      key_hash = ActiveSupport::Digest.hexdigest(key)
      key_trim_size = KEY_MAX_SIZE - key_separator.size - key_hash.size
      key = "#{key[0, key_trim_size]}#{key_separator}#{key_hash}"
    end
  end
  key
end
read_entry(key, **options) click to toggle source

Read an entry from the cache.

# File lib/active_support/cache/mem_cache_store.rb, line 182
def read_entry(key, **options)
  deserialize_entry(read_serialized_entry(key, **options), **options)
end
read_multi_entries(names, **options) click to toggle source

Reads multiple entries from the cache implementation.

# File lib/active_support/cache/mem_cache_store.rb, line 212
def read_multi_entries(names, **options)
  keys_to_names = names.index_by { |name| normalize_key(name, options) }

  raw_values = begin
    @data.with { |c| c.get_multi(keys_to_names.keys) }
  rescue Dalli::UnmarshalError
    {}
  end

  values = {}

  raw_values.each do |key, value|
    entry = deserialize_entry(value, raw: options[:raw])

    unless entry.nil? || entry.expired? || entry.mismatched?(normalize_version(keys_to_names[key], options))
      begin
        values[keys_to_names[key]] = entry.value
      rescue DeserializationError
      end
    end
  end

  values
end
read_serialized_entry(key, **options) click to toggle source
# File lib/active_support/cache/mem_cache_store.rb, line 186
def read_serialized_entry(key, **options)
  rescue_error_with(nil) do
    @data.with { |c| c.get(key, options) }
  end
end
rescue_error_with(fallback) { || ... } click to toggle source
# File lib/active_support/cache/mem_cache_store.rb, line 277
def rescue_error_with(fallback)
  yield
rescue Dalli::DalliError => error
  logger.error("DalliError (#{error}): #{error.message}") if logger
  ActiveSupport.error_reporter&.report(
    error,
    severity: :warning,
    source: "mem_cache_store.active_support",
  )
  fallback
end
serialize_entry(entry, raw: false, **options) click to toggle source
# File lib/active_support/cache/mem_cache_store.rb, line 242
def serialize_entry(entry, raw: false, **options)
  if raw
    entry.value.to_s
  else
    super(entry, raw: raw, **options)
  end
end
write_entry(key, entry, **options) click to toggle source

Write an entry to the cache.

# File lib/active_support/cache/mem_cache_store.rb, line 193
def write_entry(key, entry, **options)
  write_serialized_entry(key, serialize_entry(entry, **options), **options)
end
write_serialized_entry(key, payload, **options) click to toggle source
# File lib/active_support/cache/mem_cache_store.rb, line 197
def write_serialized_entry(key, payload, **options)
  method = options[:unless_exist] ? :add : :set
  expires_in = options[:expires_in].to_i
  if options[:race_condition_ttl] && 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
  rescue_error_with nil do
    # Don't pass compress option to Dalli since we are already dealing with compression.
    options.delete(:compress)
    @data.with { |c| !!c.send(method, key, payload, expires_in, **options) }
  end
end