Add support for connection pooling on RedisCacheStore

This commit is contained in:
fatkodima 2017-12-14 17:05:13 +02:00 committed by fatkodima
parent 2417f3c53f
commit dc407392cd
7 changed files with 101 additions and 25 deletions

View File

@ -52,7 +52,7 @@ end
gem "dalli", ">= 2.2.1" gem "dalli", ">= 2.2.1"
gem "listen", ">= 3.0.5", "< 3.2", require: false gem "listen", ">= 3.0.5", "< 3.2", require: false
gem "libxml-ruby", platforms: :ruby gem "libxml-ruby", platforms: :ruby
gem "connection_pool" gem "connection_pool", require: false
# for railties app_generator_test # for railties app_generator_test
gem "bootsnap", ">= 1.1.0", require: false gem "bootsnap", ">= 1.1.0", require: false

View File

@ -1,3 +1,5 @@
* Add support for connection pooling on RedisCacheStore.
*fatkodima*
Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activesupport/CHANGELOG.md) for previous changes. Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activesupport/CHANGELOG.md) for previous changes.

View File

@ -160,6 +160,23 @@ module ActiveSupport
attr_reader :silence, :options attr_reader :silence, :options
alias :silence? :silence alias :silence? :silence
class << self
private
def retrieve_pool_options(options)
{}.tap do |pool_options|
pool_options[:size] = options.delete(:pool_size) if options[:pool_size]
pool_options[:timeout] = options.delete(:pool_timeout) if options[:pool_timeout]
end
end
def ensure_connection_pool_added!
require "connection_pool"
rescue LoadError => e
$stderr.puts "You don't have connection_pool installed in your application. Please add it to your Gemfile and run bundle install"
raise e
end
end
# Creates a new cache. The options will be passed to any write method calls # Creates a new cache. The options will be passed to any write method calls
# except for <tt>:namespace</tt> which can be used to set the global # except for <tt>:namespace</tt> which can be used to set the global
# namespace for the cache. # namespace for the cache.

View File

@ -63,21 +63,12 @@ module ActiveSupport
addresses = addresses.flatten addresses = addresses.flatten
options = addresses.extract_options! options = addresses.extract_options!
addresses = ["localhost:11211"] if addresses.empty? addresses = ["localhost:11211"] if addresses.empty?
pool_options = retrieve_pool_options(options)
pool_options = {}
pool_options[:size] = options[:pool_size] if options[:pool_size]
pool_options[:timeout] = options[:pool_timeout] if options[:pool_timeout]
if pool_options.empty? if pool_options.empty?
Dalli::Client.new(addresses, options) Dalli::Client.new(addresses, options)
else else
begin ensure_connection_pool_added!
require "connection_pool"
rescue LoadError => e
$stderr.puts "You don't have connection_pool installed in your application. Please add it to your Gemfile and run bundle install"
raise e
end
ConnectionPool.new(pool_options) { Dalli::Client.new(addresses, options.merge(threadsafe: false)) } ConnectionPool.new(pool_options) { Dalli::Client.new(addresses, options.merge(threadsafe: false)) }
end end
end end

View File

@ -20,6 +20,15 @@ require "active_support/core_ext/marshal"
module ActiveSupport module ActiveSupport
module Cache module Cache
module ConnectionPoolLike
def with
yield self
end
end
::Redis.include(ConnectionPoolLike)
::Redis::Distributed.include(ConnectionPoolLike)
# Redis cache store. # Redis cache store.
# #
# Deployment note: Take care to use a *dedicated Redis cache* rather # Deployment note: Take care to use a *dedicated Redis cache* rather
@ -172,7 +181,16 @@ module ActiveSupport
end end
def redis def redis
@redis ||= self.class.build_redis(**redis_options) @redis ||= begin
pool_options = self.class.send(:retrieve_pool_options, redis_options)
if pool_options.any?
self.class.send(:ensure_connection_pool_added!)
::ConnectionPool.new(pool_options) { self.class.build_redis(**redis_options) }
else
self.class.build_redis(**redis_options)
end
end
end end
def inspect def inspect
@ -211,7 +229,7 @@ module ActiveSupport
instrument :delete_matched, matcher do instrument :delete_matched, matcher do
case matcher case matcher
when String when String
redis.eval DELETE_GLOB_LUA, [], [namespace_key(matcher, options)] redis.with { |c| c.eval DELETE_GLOB_LUA, [], [namespace_key(matcher, options)] }
else else
raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}" raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}"
end end
@ -229,7 +247,7 @@ module ActiveSupport
def increment(name, amount = 1, options = nil) def increment(name, amount = 1, options = nil)
instrument :increment, name, amount: amount do instrument :increment, name, amount: amount do
failsafe :increment do failsafe :increment do
redis.incrby normalize_key(name, options), amount redis.with { |c| c.incrby normalize_key(name, options), amount }
end end
end end
end end
@ -245,7 +263,7 @@ module ActiveSupport
def decrement(name, amount = 1, options = nil) def decrement(name, amount = 1, options = nil)
instrument :decrement, name, amount: amount do instrument :decrement, name, amount: amount do
failsafe :decrement do failsafe :decrement do
redis.decrby normalize_key(name, options), amount redis.with { |c| c.decrby normalize_key(name, options), amount }
end end
end end
end end
@ -267,7 +285,7 @@ module ActiveSupport
if namespace = merged_options(options)[namespace] if namespace = merged_options(options)[namespace]
delete_matched "*", namespace: namespace delete_matched "*", namespace: namespace
else else
redis.flushdb redis.with { |c| c.flushdb }
end end
end end
end end
@ -298,7 +316,7 @@ module ActiveSupport
# Read an entry from the cache. # Read an entry from the cache.
def read_entry(key, options = nil) def read_entry(key, options = nil)
failsafe :read_entry do failsafe :read_entry do
deserialize_entry redis.get(key) deserialize_entry redis.with { |c| c.get(key) }
end end
end end
@ -309,7 +327,7 @@ module ActiveSupport
keys = names.map { |name| normalize_key(name, options) } keys = names.map { |name| normalize_key(name, options) }
values = failsafe(:read_multi_mget, returning: {}) do values = failsafe(:read_multi_mget, returning: {}) do
redis.mget(*keys) redis.with { |c| c.mget(*keys) }
end end
names.zip(values).each_with_object({}) do |(name, value), results| names.zip(values).each_with_object({}) do |(name, value), results|
@ -341,9 +359,9 @@ module ActiveSupport
modifiers[:nx] = unless_exist modifiers[:nx] = unless_exist
modifiers[:px] = (1000 * expires_in.to_f).ceil if expires_in modifiers[:px] = (1000 * expires_in.to_f).ceil if expires_in
redis.set key, value, modifiers redis.with { |c| c.set key, value, modifiers }
else else
redis.set key, value redis.with { |c| c.set key, value }
end end
end end
end end
@ -351,7 +369,7 @@ module ActiveSupport
# Delete an entry from the cache. # Delete an entry from the cache.
def delete_entry(key, options) def delete_entry(key, options)
failsafe :delete_entry, returning: false do failsafe :delete_entry, returning: false do
redis.del key redis.with { |c| c.del key }
end end
end end
@ -360,7 +378,7 @@ module ActiveSupport
if entries.any? if entries.any?
if mset_capable? && expires_in.nil? if mset_capable? && expires_in.nil?
failsafe :write_multi_entries do failsafe :write_multi_entries do
redis.mapped_mset(entries) redis.with { |c| c.mapped_mset(entries) }
end end
else else
super super

View File

@ -6,7 +6,7 @@ module ConnectionPoolBehavior
emulating_latency do emulating_latency do
begin begin
cache = ActiveSupport::Cache.lookup_store(store, pool_size: 2, pool_timeout: 1) cache = ActiveSupport::Cache.lookup_store(store, { pool_size: 2, pool_timeout: 1 }.merge(store_options))
cache.clear cache.clear
threads = [] threads = []
@ -33,7 +33,7 @@ module ConnectionPoolBehavior
def test_no_connection_pool def test_no_connection_pool
emulating_latency do emulating_latency do
begin begin
cache = ActiveSupport::Cache.lookup_store(store) cache = ActiveSupport::Cache.lookup_store(store, store_options)
cache.clear cache.clear
threads = [] threads = []
@ -54,4 +54,7 @@ module ConnectionPoolBehavior
end end
end end
end end
private
def store_options; {}; end
end end

View File

@ -5,6 +5,24 @@ require "active_support/cache"
require "active_support/cache/redis_cache_store" require "active_support/cache/redis_cache_store"
require_relative "../behaviors" require_relative "../behaviors"
driver_name = %w[ ruby hiredis ].include?(ENV["REDIS_DRIVER"]) ? ENV["REDIS_DRIVER"] : "hiredis"
driver = Object.const_get("Redis::Connection::#{driver_name.camelize}")
Redis::Connection.drivers.clear
Redis::Connection.drivers.append(driver)
# Emulates a latency on Redis's back-end for the key latency to facilitate
# connection pool testing.
class SlowRedis < Redis
def get(key, options = {})
if key =~ /latency/
sleep 3
else
super
end
end
end
module ActiveSupport::Cache::RedisCacheStoreTests module ActiveSupport::Cache::RedisCacheStoreTests
DRIVER = %w[ ruby hiredis ].include?(ENV["REDIS_DRIVER"]) ? ENV["REDIS_DRIVER"] : "hiredis" DRIVER = %w[ ruby hiredis ].include?(ENV["REDIS_DRIVER"]) ? ENV["REDIS_DRIVER"] : "hiredis"
@ -110,6 +128,33 @@ module ActiveSupport::Cache::RedisCacheStoreTests
include AutoloadingCacheBehavior include AutoloadingCacheBehavior
end end
class ConnectionPoolBehaviourTest < StoreTest
include ConnectionPoolBehavior
private
def store
:redis_cache_store
end
def emulating_latency
old_redis = Object.send(:remove_const, :Redis)
Object.const_set(:Redis, SlowRedis)
yield
ensure
Object.send(:remove_const, :Redis)
Object.const_set(:Redis, old_redis)
end
end
class RedisDistributedConnectionPoolBehaviourTest < ConnectionPoolBehaviourTest
private
def store_options
{ url: %w[ redis://localhost:6379/0 redis://localhost:6379/0 ] }
end
end
# Separate test class so we can omit the namespace which causes expected, # Separate test class so we can omit the namespace which causes expected,
# appropriate complaints about incompatible string encodings. # appropriate complaints about incompatible string encodings.
class KeyEncodingSafetyTest < StoreTest class KeyEncodingSafetyTest < StoreTest