Class: ActiveSupport::Cache::RedisCacheStore
| Relationships & Source Files | |
| Super Chains via Extension / Inclusion / Inheritance | |
|
Class Chain:
self,
Store
|
|
|
Instance Chain:
self,
Strategy::LocalCache,
Store
|
|
| Inherits: |
ActiveSupport::Cache::Store
|
| Defined in: | activesupport/lib/active_support/cache/redis_cache_store.rb |
Overview
Redis Cache Store
Deployment note: Take care to use a dedicated Redis cache rather than pointing this at a persistent Redis server (for example, one used as an Active Job queue). Redis won't cope well with mixed usage patterns and it won't expire cache entries by default.
Redis cache server setup guide: https://redis.io/topics/lru-cache
-
Supports vanilla Redis, hiredis, and
Redis::Distributed. -
Supports Memcached-like sharding across Redises with
Redis::Distributed. -
Fault tolerant. If the Redis server is unavailable, no exceptions are raised. Cache fetches are all misses and writes are dropped.
-
Local cache. Hot in-memory primary cache within block/middleware scope.
-
#read_multi and
write_multisupport for Redis mget/mset. UseRedis::Distributed4.0.1+ for distributed mget support. -
#delete_matched support for Redis KEYS globs.
-
readsupports delete: true to atomically read and delete a cache entry using the RedisGETDELcommand.cache.write("greeting", "hello") cache.read("greeting", delete: true) # => "hello" cache.read("greeting") # => nil
Constant Summary
-
DEFAULT_ERROR_HANDLER =
# File 'activesupport/lib/active_support/cache/redis_cache_store.rb', line 54#=> (method:, returning:, exception:) do if logger logger.error { "RedisCacheStore: #{method} failed, returned #{returning.inspect}: #{exception.class}: #{exception.}" } end ActiveSupport.error_reporter&.report( exception, severity: :warning, source: "redis_cache_store.active_support", ) end
-
DEFAULT_REDIS_OPTIONS =
# File 'activesupport/lib/active_support/cache/redis_cache_store.rb', line 48{ connect_timeout: 1, read_timeout: 1, write_timeout: 1, }.freeze -
SCAN_BATCH_SIZE =
private
# File 'activesupport/lib/active_support/cache/redis_cache_store.rb', line 186
The maximum number of entries to receive per SCAN call.
1000
Store - Inherited
Class Attribute Summary
-
.supports_cache_versioning? ⇒ Boolean
readonly
Advertise cache versioning support.
Store - Inherited
Class Method Summary
- .new(**options) ⇒ RedisCacheStore constructor
Store - Inherited
| .new | Creates a new cache. |
| .retrieve_pool_options | |
Instance Attribute Summary
- #redis readonly
- #supports_expire_nx? ⇒ Boolean readonly private
Store - Inherited
| #logger, | |
| #namespace | Get the current namespace. |
| #namespace= | Set the current namespace. |
| #options, #raise_on_invalid_cache_expiration_time, | |
| #silence? | Alias for Store#silence. |
Instance Method Summary
-
#cleanup(options = nil)
::ActiveSupport::CacheStore API implementation. -
#clear(options = nil)
Clear the entire cache on all Redis servers.
-
#decrement(name, amount = 1, options = nil)
Decrement a cached integer value using the Redis decrby atomic operator.
-
#delete_matched(matcher, options = nil)
::ActiveSupport::CacheStore API implementation. -
#increment(name, amount = 1, options = nil)
Increment a cached integer value using the Redis incrby atomic operator.
-
#initialize(error_handler: DEFAULT_ERROR_HANDLER, **redis_options) ⇒ RedisCacheStore
constructor
Creates a new Redis cache store.
-
#read_multi(*names)
::ActiveSupport::CacheStore API implementation. -
#stats
Get info from redis servers.
- #change_counter(key, amount, options) private
-
#delete_entry(key, **options)
private
Delete an entry from the cache.
-
#delete_multi_entries(entries, **_options)
private
Deletes multiple entries in the cache.
- #deserialize_entry(payload, raw: false) private
- #failsafe(method, returning: nil) private
- #instance_variables_to_inspect private
-
#read_entry(key, **options)
private
Storeprovider interface: Read an entry from the cache. - #read_multi_entries(names, **options) private
- #read_serialized_entry(key, raw: false, **options) private
- #serialize_entries(entries, **options) private
- #serialize_entry(entry, raw: false, **options) private
-
#write_entry(key, entry, raw: false, **options)
private
Write an entry to the cache.
-
#write_multi_entries(entries, **options)
private
Nonstandard store provider API to write multiple values at once.
- #write_serialized_entry(key, payload, raw: false, unless_exist: false, expires_in: nil, race_condition_ttl: nil, pipeline: nil, **options) private
Strategy::LocalCache - Included
| #local_cache | The current local cache. |
| #middleware | Middleware class can be inserted as a |
| #new_local_cache | Set a new local cache. |
| #unset_local_cache | Unset the current local cache. |
| #with_local_cache | Use a local cache for the duration of block. |
| #bypass_local_cache, #delete_entry, #local_cache_key, #read_multi_entries, #read_serialized_entry, #use_temporary_local_cache, #write_cache_value, #write_serialized_entry, #cleanup, #clear, #decrement, #delete_matched, #fetch_multi, #increment | |
Store - Inherited
| #cleanup | Cleans up the cache by removing expired entries. |
| #clear | Clears the entire cache. |
| #decrement | Decrements an integer value in the cache. |
| #delete | Deletes an entry in the cache. |
| #delete_matched | Deletes all entries with keys matching the pattern. |
| #delete_multi | Deletes multiple entries in the cache. |
| #exist? | Returns |
| #fetch | Fetches data from the cache, using the given key. |
| #fetch_multi | Fetches data from the cache, using the given keys. |
| #increment | Increments an integer value in the cache. |
| #mute | Silences the logger within a block. |
| #read | Reads data from the cache, using the given key. |
| #read_counter | Reads a counter that was set by #increment / #decrement. |
| #read_multi | Reads multiple values at once from the cache. |
| #silence, | |
| #silence! | Silences the logger. |
| #write | Writes the value to the cache with the key. |
| #write_counter | Writes a counter that can then be modified by #increment / #decrement. |
| #write_multi |
|
| #_instrument, #default_serializer, | |
| #delete_entry | Deletes an entry from the cache implementation. |
| #delete_multi_entries | Deletes multiples entries in the cache implementation. |
| #deserialize_entry, #expand_and_namespace_key, | |
| #expanded_key | Expands key to be a consistent string value. |
| #expanded_version, #get_entry_value, #handle_expired_entry, #handle_invalid_expires_in, #instrument, #instrument_multi, | |
| #key_matcher | Adds the namespace defined in the options to a pattern designed to match keys. |
| #merged_options | Merges the default options with ones specific to a method call. |
| #namespace_key | Prefix the key with a namespace string: |
| #normalize_key | Expands, namespaces and truncates the cache key. |
| #normalize_options | Normalize aliased options to their canonical form. |
| #normalize_version, | |
| #read_entry | Reads an entry from the cache implementation. |
| #read_multi_entries | Reads multiple entries from the cache implementation. |
| #save_block_result_to_cache, #serialize_entry, #truncate_key, #validate_options, | |
| #write_entry | Writes an entry to the cache implementation. |
| #write_multi_entries | Writes multiple entries to the cache implementation. |
| #new_entry | |
Constructor Details
.new(**options) ⇒ RedisCacheStore
# File 'activesupport/lib/active_support/cache/redis_cache_store.rb', line 70
def self.new(**) if [:redis] ActiveSupport.deprecator.warn(<<~MSG.squish) Passing a Redis or ConnectionPool instance via the `:redis` configuration to ActiveSupport::Cache::RedisCacheStore is deprecated and will be removed in Rails 8.3. RedisCacheStore no longer depends on the `redis` gem, but use the simpler `redis-client`. Prefer passing a raw `:url` option instead, of if you need more advanced configuration, pass a configured `RedisClient` via the `:client` option. MSG return DeprecatedRedisCacheStore.new(**) end super end
#initialize(error_handler: DEFAULT_ERROR_HANDLER, **redis_options) ⇒ RedisCacheStore
Creates a new Redis cache store.
The :url param can be:
- A string used to create a RedisClient::Pooled instance.
- An array of strings used to create a {RedisClient::HashRing} instance.
Option Class Result
:url String -> RedisClient.config(url: …).new_pool
:url Array -> RedisClient::HashRing.new([RedisClient.config(url: …).new_pool, ...])
If you need some advanced configuration for the client, or want to use an alternative implementation
like redis-cluster-client, you can pass an already configued client via the :client option:
config.cache_store = :redis_cache_store, client: RedisClient.config(...)
config.cache_store = :redis_cache_store, client: [RedisClient.config(...), RedisClient.config(...)]
config.cache_store = :redis_cache_store, client: -> { RedisClient.config(...) }
No namespace is set by default. Provide one if the Redis cache server is shared with other apps: namespace: 'myapp-cache'.
Compression is enabled by default with a 1kB threshold, so cached values larger than 1kB are automatically compressed. Disable by passing compress: false or change the threshold by passing compress_threshold: 4.kilobytes.
No expiry is set on cache entries by default. Redis is expected to be configured with an eviction policy that automatically deletes least-recently or -frequently used keys when it reaches max memory. See https://redis.io/topics/lru-cache for cache server setup.
Race condition TTL is not set by default. This can be used to avoid "thundering herd" cache writes when hot cache entries are expired. See Store#fetch for more.
Setting skip_nil: true will not cache nil results:
cache.fetch('foo') { nil }
cache.fetch('bar', skip_nil: true) { nil }
cache.exist?('foo') # => true
cache.exist?('bar') # => false
# File 'activesupport/lib/active_support/cache/redis_cache_store.rb', line 131
def initialize(error_handler: DEFAULT_ERROR_HANDLER, **) = .extract!(*UNIVERSAL_OPTIONS) = self.class.send(:, ) if .key?(:client) client = .delete(:client) clients = Array.wrap(client.respond_to?(:call) ? client.call : client) else urls = Array.wrap(.delete(:url)) urls << nil if urls.empty? clients = urls.map do |url| RedisClient.config(url: url, protocol: 2, **) end end clients.map! do |c| if c.respond_to?(:new_pool) c.new_pool(**( || {})) else c end end @redis = if clients.size > 1 RedisClient.ring(clients) else clients.first end @error_handler = error_handler super() end
Class Attribute Details
.supports_cache_versioning? ⇒ Boolean (readonly)
Advertise cache versioning support.
# File 'activesupport/lib/active_support/cache/redis_cache_store.rb', line 66
def self.supports_cache_versioning? true end
Instance Attribute Details
#redis (readonly)
[ GitHub ]# File 'activesupport/lib/active_support/cache/redis_cache_store.rb', line 89
attr_reader :redis
#supports_expire_nx? ⇒ Boolean (readonly, private)
[ GitHub ]
# File 'activesupport/lib/active_support/cache/redis_cache_store.rb', line 473
def supports_expire_nx? return @supports_expire_nx if defined?(@supports_expire_nx) redis_versions = redis.nodes.map { |n| n.call("info", "server").scan(/edis_version:([\d.]+)/)[0][0] || "0" } @supports_expire_nx = redis_versions.all? { |v| Gem::Version.new(v) >= Gem::Version.new("7.0.0") } end
Instance Method Details
#change_counter(key, amount, options) (private)
[ GitHub ]# File 'activesupport/lib/active_support/cache/redis_cache_store.rb', line 448
def change_counter(key, amount, ) redis.node_for(key).with do |c| expires_in = [:expires_in] if expires_in if supports_expire_nx? count, _ = c.pipelined do |pipeline| pipeline.call("incrby", key, amount) pipeline.call("expire", key, expires_in.to_i, "NX") end else count, ttl = c.pipelined do |pipeline| pipeline.call("incrby", key, amount) pipeline.call("ttl", key) end c.call("expire", key, expires_in.to_i) if ttl < 0 end else count = c.call("incrby", key, amount) end count end end
#cleanup(options = nil)
::ActiveSupport::Cache Store API implementation.
Removes expired entries. Handled natively by Redis least-recently-/ least-frequently-used expiry, so manual cleanup is not supported.
# File 'activesupport/lib/active_support/cache/redis_cache_store.rb', line 290
def cleanup( = nil) super end
#clear(options = nil)
Clear the entire cache on all Redis servers. Safe to use on shared servers if the cache is namespaced.
Failsafe: Raises errors.
# File 'activesupport/lib/active_support/cache/redis_cache_store.rb', line 298
def clear( = nil) failsafe :clear do if namespace = ()[:namespace] delete_matched "*", namespace: namespace else redis.then { |c| c.flushdb } end end end
#decrement(name, amount = 1, options = nil)
Decrement a cached integer value using the Redis decrby atomic operator. Returns the updated value.
If the key is unset or has expired, it will be set to -amount:
cache.decrement("foo") # => -1
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.
To read the value later, call #read_counter:
cache.decrement("baz") # => 3
cache.read_counter("baz") # 3
Failsafe: Raises errors.
# File 'activesupport/lib/active_support/cache/redis_cache_store.rb', line 275
def decrement(name, amount = 1, = nil) = () key = normalize_key(name, ) instrument :decrement, key, amount: amount do failsafe :decrement do change_counter(key, -amount, ) end end end
#delete_entry(key, **options) (private)
Delete an entry from the cache.
#delete_matched(matcher, options = nil)
::ActiveSupport::Cache Store API implementation.
Supports Redis KEYS glob patterns:
h?llo matches hello, hallo and hxllo
h*llo matches hllo and heeeello
h[ae]llo matches hello and hallo, but not hillo
h[^e]llo matches hallo, hbllo, ... but not hello
h[a-b]llo matches hallo and hbllo
Use \ to escape special characters if you want to match them verbatim.
See https://redis.io/commands/KEYS for more.
Failsafe: Raises errors.
# File 'activesupport/lib/active_support/cache/redis_cache_store.rb', line 204
def delete_matched(matcher, = nil) unless String === matcher raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}" end pattern = namespace_key(matcher, ) instrument :delete_matched, pattern do redis.nodes.each do |node| node.with do |conn| conn.scan(match: pattern, count: SCAN_BATCH_SIZE).each_slice(SCAN_BATCH_SIZE).each do |keys| conn.call("unlink", *keys) end end end end end
#delete_multi_entries(entries, **_options) (private)
Deletes multiple entries in the cache. Returns the number of entries deleted.
#deserialize_entry(payload, raw: false) (private)
[ GitHub ]#failsafe(method, returning: nil) (private)
[ GitHub ]# File 'activesupport/lib/active_support/cache/redis_cache_store.rb', line 480
def failsafe(method, returning: nil) yield rescue ::RedisClient::ConnectionError => error @error_handler&.call(method: method, exception: error, returning: returning) returning end
#increment(name, amount = 1, options = nil)
Increment a cached integer value using the Redis incrby 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.
To read the value later, call #read_counter:
cache.increment("baz") # => 7
cache.read_counter("baz") # 7
Failsafe: Raises errors.
# File 'activesupport/lib/active_support/cache/redis_cache_store.rb', line 243
def increment(name, amount = 1, = nil) = () key = normalize_key(name, ) instrument :increment, key, amount: amount do failsafe :increment do change_counter(key, amount, ) end end end
#instance_variables_to_inspect (private)
[ GitHub ]# File 'activesupport/lib/active_support/cache/redis_cache_store.rb', line 314
def instance_variables_to_inspect [:@options, :@redis].freeze end
#read_entry(key, **options) (private)
Store provider interface:
Read an entry from the cache.
# File 'activesupport/lib/active_support/cache/redis_cache_store.rb', line 320
def read_entry(key, **) deserialize_entry(read_serialized_entry(key, **), **) end
#read_multi(*names)
::ActiveSupport::Cache Store API implementation.
Read multiple values at once. Returns a hash of requested keys -> fetched values.
# File 'activesupport/lib/active_support/cache/redis_cache_store.rb', line 171
def read_multi(*names) return {} if names.empty? = names. = () keys = names.map { |name| normalize_key(name, ) } instrument_multi(:read_multi, keys, ) do |payload| read_multi_entries(names, **).tap do |results| payload[:hits] = results.keys.map { |name| normalize_key(name, ) } end end end
#read_multi_entries(names, **options) (private)
[ GitHub ]# File 'activesupport/lib/active_support/cache/redis_cache_store.rb', line 331
def read_multi_entries(names, **) = () return {} if names == [] raw = &.fetch(:raw, false) keys = names.map { |name| normalize_key(name, ) } keys_index = keys.each_with_index.to_h results = {} redis.nodes_for(keys).each do |node, key_subset| failsafe(:read_multi_entries) do node.call("mget", *key_subset).each_with_index do |value, index| if value results[names[keys_index[key_subset[index]]]] = value end end end end results.transform_values! { |value| deserialize_entry(value, raw: raw) } results.reject! do |name, entry| entry.nil? || entry.expired? || entry.mismatched?(normalize_version(name, )) end results.compact! results.transform_values! { |entry| entry&.value } results end
#read_serialized_entry(key, raw: false, **options) (private)
[ GitHub ]# File 'activesupport/lib/active_support/cache/redis_cache_store.rb', line 324
def read_serialized_entry(key, raw: false, **) failsafe :read_entry do command = [:delete] ? "getdel" : "get" redis.node_for(key).call(command, key) end end
#serialize_entries(entries, **options) (private)
[ GitHub ]# File 'activesupport/lib/active_support/cache/redis_cache_store.rb', line 442
def serialize_entries(entries, **) entries.transform_values do |entry| serialize_entry(entry, **) end end
#serialize_entry(entry, raw: false, **options) (private)
[ GitHub ]# File 'activesupport/lib/active_support/cache/redis_cache_store.rb', line 434
def serialize_entry(entry, raw: false, **) if raw entry.value.to_s else super(entry, raw: raw, **) end end
#stats
Get info from redis servers.
# File 'activesupport/lib/active_support/cache/redis_cache_store.rb', line 309
def stats redis.then { |c| c.info } end
#write_entry(key, entry, raw: false, **options) (private)
Write an entry to the cache.
Requires Redis 2.6.12+ for extended SET options.
# File 'activesupport/lib/active_support/cache/redis_cache_store.rb', line 363
def write_entry(key, entry, raw: false, **) write_serialized_entry(key, serialize_entry(entry, raw: raw, **), raw: raw, **) end
#write_multi_entries(entries, **options) (private)
Nonstandard store provider API to write multiple values at once.
# File 'activesupport/lib/active_support/cache/redis_cache_store.rb', line 412
def write_multi_entries(entries, **) return if entries.empty? redis.nodes_for(entries.keys).each do |node, keys| failsafe :write_multi_entries do node.pipelined do |pipeline| entries.slice(*keys).each do |key, entry| write_entry key, entry, **, pipeline: pipeline end end end end end
#write_serialized_entry(key, payload, raw: false, unless_exist: false, expires_in: nil, race_condition_ttl: nil, pipeline: nil, **options) (private)
[ GitHub ]# File 'activesupport/lib/active_support/cache/redis_cache_store.rb', line 367
def write_serialized_entry(key, payload, raw: false, unless_exist: false, expires_in: nil, race_condition_ttl: nil, pipeline: nil, **) # If race condition TTL is in use, ensure that cache entries # stick around a bit longer after they would have expired # so we can purposefully serve stale entries. if race_condition_ttl && expires_in && expires_in > 0 && !raw expires_in += 5.minutes end modifiers = [] if unless_exist || expires_in modifiers << :nx if unless_exist modifiers << :px << (1000 * expires_in.to_f).ceil if expires_in end if pipeline pipeline.call("set", key, payload, *modifiers) else failsafe :write_entry, returning: nil do redis.node_for(key).call("set", key, payload, *modifiers) == "OK" end end end