Add a proxy to deal with ActiveSupport::Cache::MemCacheStore

If connection pooling is used with AS::Cache::MemCacheStore,
unwrap_active_support_stores wouldn't return the underlying dalli instance(s),
and so Rack::Attack.store would be the bare unproxied MemCacheStore instance.

Calling write then increment would silently fail because :raw wasn't used.

With this commit, we no longer try to unwrap AS::Cache::MemCacheStore instances.
This commit is contained in:
Jonathan del Strother 2018-09-01 17:15:05 +01:00
parent 03b8ce6f9e
commit 5cdc15b35a
5 changed files with 47 additions and 15 deletions

View file

@ -19,6 +19,7 @@ class Rack::Attack
autoload :StoreProxy, 'rack/attack/store_proxy'
autoload :DalliProxy, 'rack/attack/store_proxy/dalli_proxy'
autoload :MemCacheProxy, 'rack/attack/store_proxy/mem_cache_proxy'
autoload :MemCacheStoreProxy, 'rack/attack/store_proxy/mem_cache_store_proxy'
autoload :RedisProxy, 'rack/attack/store_proxy/redis_proxy'
autoload :RedisStoreProxy, 'rack/attack/store_proxy/redis_store_proxy'
autoload :RedisCacheStoreProxy, 'rack/attack/store_proxy/redis_cache_store_proxy'

View file

@ -3,10 +3,7 @@
module Rack
class Attack
module StoreProxy
PROXIES = [DalliProxy, MemCacheProxy, RedisStoreProxy, RedisProxy, RedisCacheStoreProxy].freeze
ACTIVE_SUPPORT_WRAPPER_CLASSES = Set.new(['ActiveSupport::Cache::MemCacheStore', 'ActiveSupport::Cache::RedisStore', 'ActiveSupport::Cache::RedisCacheStore']).freeze
ACTIVE_SUPPORT_CLIENTS = Set.new(['Redis::Store', 'Dalli::Client', 'MemCache']).freeze
PROXIES = [DalliProxy, MemCacheStoreProxy, MemCacheProxy, RedisStoreProxy, RedisProxy, RedisCacheStoreProxy].freeze
def self.build(store)
client = unwrap_active_support_stores(store)
@ -17,15 +14,8 @@ module Rack
def self.unwrap_active_support_stores(store)
# ActiveSupport::Cache::RedisStore doesn't expose any way to set an expiry,
# so use the raw Redis::Store instead.
# We also want to use the underlying Dalli client instead of ::ActiveSupport::Cache::MemCacheStore,
# and the MemCache client if using Rails 3.x
if store.instance_variable_defined?(:@data)
client = store.instance_variable_get(:@data)
end
if ACTIVE_SUPPORT_WRAPPER_CLASSES.include?(store.class.to_s) && ACTIVE_SUPPORT_CLIENTS.include?(client.class.to_s)
client
if store.class.name == 'ActiveSupport::Cache::RedisStore'
store.instance_variable_get(:@data)
else
store
end

View file

@ -0,0 +1,19 @@
# frozen_string_literal: true
require 'delegate'
module Rack
class Attack
module StoreProxy
class MemCacheStoreProxy < SimpleDelegator
def self.handle?(store)
defined?(::Dalli) && defined?(::ActiveSupport::Cache::MemCacheStore) && store.is_a?(::ActiveSupport::Cache::MemCacheStore)
end
def write(name, value, options = {})
super(name, value, options.merge!(raw: true))
end
end
end
end
end

View file

@ -0,0 +1,22 @@
# frozen_string_literal: true
require_relative "../../spec_helper"
if defined?(::ConnectionPool) && defined?(::Dalli)
require_relative "../../support/cache_store_helper"
require "timecop"
describe "ActiveSupport::Cache::MemCacheStore (pooled) as a cache backend" do
before do
Rack::Attack.cache.store = ActiveSupport::Cache::MemCacheStore.new(pool_size: 2)
end
after do
Rack::Attack.cache.store.clear
end
it_works_for_cache_backed_features(fetch_from_store: ->(key) {
Rack::Attack.cache.store.read(key)
})
end
end

View file

@ -12,9 +12,9 @@ if defined?(::Dalli)
end
after do
Rack::Attack.cache.store.flush_all
Rack::Attack.cache.store.clear
end
it_works_for_cache_backed_features(fetch_from_store: ->(key) { Rack::Attack.cache.store.get(key) })
it_works_for_cache_backed_features(fetch_from_store: ->(key) { Rack::Attack.cache.store.read(key) })
end
end