Skip to content

Commit c07812c

Browse files
committed
Eagerly validate pool arguments in Redis and MemCache stores
Fix: rails#48352 While we should ensure instantiating the store doesn't immediately attempt to connect, we should eagerly process arguments so that if they are somehow invalid we fail early during boot rather than at runtime. Additionally, since it's common to get pool parameters from environment variable, we can use `Integer` and `Float` so that string representations are valid.
1 parent 254f1d8 commit c07812c

File tree

5 files changed

+77
-44
lines changed

5 files changed

+77
-44
lines changed

activesupport/lib/active_support/cache.rb

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -186,35 +186,44 @@ class << self
186186
private_constant :DEFAULT_POOL_OPTIONS
187187

188188
def retrieve_pool_options(options)
189-
unless options.key?(:pool) || options.key?(:pool_size) || options.key?(:pool_timeout)
190-
options[:pool] = true
191-
end
189+
if options.key?(:pool)
190+
pool_options = options.delete(:pool)
191+
elsif options.key?(:pool_size) || options.key?(:pool_timeout)
192+
pool_options = {}
193+
194+
if options.key?(:pool_size)
195+
ActiveSupport.deprecator.warn(<<~MSG)
196+
Using :pool_size is deprecated and will be removed in Rails 7.2.
197+
Use `pool: { size: #{options[:pool_size].inspect} }` instead.
198+
MSG
199+
pool_options[:size] = options.delete(:pool_size)
200+
end
192201

193-
if (pool_options = options.delete(:pool))
194-
if Hash === pool_options
195-
DEFAULT_POOL_OPTIONS.merge(pool_options)
196-
else
197-
DEFAULT_POOL_OPTIONS
202+
if options.key?(:pool_timeout)
203+
ActiveSupport.deprecator.warn(<<~MSG)
204+
Using :pool_timeout is deprecated and will be removed in Rails 7.2.
205+
Use `pool: { timeout: #{options[:pool_timeout].inspect} }` instead.
206+
MSG
207+
pool_options[:timeout] = options.delete(:pool_timeout)
198208
end
199209
else
200-
{}.tap do |pool_options|
201-
if options[:pool_size]
202-
ActiveSupport.deprecator.warn(<<~MSG)
203-
Using :pool_size is deprecated and will be removed in Rails 7.2.
204-
Use `pool: { size: #{options[:pool_size].inspect} }` instead.
205-
MSG
206-
pool_options[:size] = options.delete(:pool_size)
207-
end
208-
209-
if options[:pool_timeout]
210-
ActiveSupport.deprecator.warn(<<~MSG)
211-
Using :pool_timeout is deprecated and will be removed in Rails 7.2.
212-
Use `pool: { timeout: #{options[:pool_timeout].inspect} }` instead.
213-
MSG
214-
pool_options[:timeout] = options.delete(:pool_timeout)
215-
end
216-
end
210+
pool_options = true
217211
end
212+
213+
case pool_options
214+
when false, nil
215+
return false
216+
when true
217+
pool_options = DEFAULT_POOL_OPTIONS
218+
when Hash
219+
pool_options[:size] = Integer(pool_options[:size]) if pool_options.key?(:size)
220+
pool_options[:timeout] = Float(pool_options[:timeout]) if pool_options.key?(:timeout)
221+
pool_options = DEFAULT_POOL_OPTIONS.merge(pool_options)
222+
else
223+
raise TypeError, "Invalid :pool argument, expected Hash, got: #{pool_options.inspect}"
224+
end
225+
226+
pool_options unless pool_options.empty?
218227
end
219228
end
220229

activesupport/lib/active_support/cache/mem_cache_store.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,10 @@ def self.build_mem_cache(*addresses) # :nodoc:
9999
addresses = nil if addresses.compact.empty?
100100
pool_options = retrieve_pool_options(options)
101101

102-
if pool_options.empty?
103-
Dalli::Client.new(addresses, options)
104-
else
102+
if pool_options
105103
ConnectionPool.new(pool_options) { Dalli::Client.new(addresses, options.merge(threadsafe: false)) }
104+
else
105+
Dalli::Client.new(addresses, options)
106106
end
107107
end
108108

activesupport/lib/active_support/cache/redis_cache_store.rb

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ def build_redis_client(**redis_options)
105105

106106
attr_reader :redis_options
107107
attr_reader :max_key_bytesize
108+
attr_reader :redis
108109

109110
# Creates a new Redis cache store.
110111
#
@@ -141,7 +142,11 @@ def build_redis_client(**redis_options)
141142
# cache.exist?('foo') # => true
142143
# cache.exist?('bar') # => false
143144
def initialize(namespace: nil, compress: true, compress_threshold: 1.kilobyte, coder: default_coder, expires_in: nil, race_condition_ttl: nil, error_handler: DEFAULT_ERROR_HANDLER, skip_nil: false, **redis_options)
144-
@redis_options = redis_options
145+
if pool_options = self.class.send(:retrieve_pool_options, redis_options)
146+
@redis = ::ConnectionPool.new(pool_options) { self.class.build_redis(**redis_options) }
147+
else
148+
@redis = self.class.build_redis(**redis_options)
149+
end
145150

146151
@max_key_bytesize = MAX_KEY_BYTESIZE
147152
@error_handler = error_handler
@@ -153,21 +158,8 @@ def initialize(namespace: nil, compress: true, compress_threshold: 1.kilobyte, c
153158
coder: coder, skip_nil: skip_nil
154159
end
155160

156-
def redis
157-
@redis ||= begin
158-
pool_options = self.class.send(:retrieve_pool_options, redis_options)
159-
160-
if pool_options.any?
161-
::ConnectionPool.new(pool_options) { self.class.build_redis(**redis_options) }
162-
else
163-
self.class.build_redis(**redis_options)
164-
end
165-
end
166-
end
167-
168161
def inspect
169-
instance = @redis || @redis_options
170-
"#<#{self.class} options=#{options.inspect} redis=#{instance.inspect}>"
162+
"#<#{self.class} options=#{options.inspect} redis=#{redis.inspect}>"
171163
end
172164

173165
# Cache Store API implementation.

activesupport/test/cache/stores/mem_cache_store_test.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,22 @@ def after_teardown
8686
include ConnectionPoolBehavior
8787
include FailureSafetyBehavior
8888

89+
test "validate pool arguments" do
90+
assert_raises TypeError do
91+
ActiveSupport::Cache::MemCacheStore.new(pool: { size: [] })
92+
end
93+
94+
assert_raises TypeError do
95+
ActiveSupport::Cache::MemCacheStore.new(pool: { timeout: [] })
96+
end
97+
98+
ActiveSupport::Cache::MemCacheStore.new(pool: { size: "12", timeout: "1.5" })
99+
end
100+
101+
test "instantiating the store doesn't connect to Memcache" do
102+
ActiveSupport::Cache::MemCacheStore.new("memcached://localhost:1")
103+
end
104+
89105
# Overrides test from LocalCacheBehavior in order to stub out the cache clear
90106
# and replace it with a delete.
91107
def test_clear_also_clears_local_cache

activesupport/test/cache/stores/redis_cache_store_test.rb

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,25 @@ class InitializationTest < ActiveSupport::TestCase
110110
assert_same @cache.redis, redis_instance
111111
end
112112

113+
test "validate pool arguments" do
114+
assert_raises TypeError do
115+
build(url: REDIS_URL, pool: { size: [] })
116+
end
117+
118+
assert_raises TypeError do
119+
build(url: REDIS_URL, pool: { timeout: [] })
120+
end
121+
122+
build(url: REDIS_URL, pool: { size: "12", timeout: "1.5" })
123+
end
124+
125+
test "instantiating the store doesn't connect to Redis" do
126+
build(url: "redis://localhost:1")
127+
end
128+
113129
private
114130
def build(**kwargs)
115-
ActiveSupport::Cache::RedisCacheStore.new(**kwargs.merge(pool: false)).tap(&:redis)
131+
ActiveSupport::Cache::RedisCacheStore.new(pool: false, **kwargs).tap(&:redis)
116132
end
117133
end
118134

0 commit comments

Comments
 (0)