diff --git a/Gemfile b/Gemfile index cea81923e..01e4e67e0 100644 --- a/Gemfile +++ b/Gemfile @@ -28,6 +28,7 @@ gem 'mysql2' gem 'pg' gem 'cuprite' gem 'puma' +gem 'connection_pool' group(:guard) do gem 'guard' diff --git a/lib/flipper/adapters/redis_pool.rb b/lib/flipper/adapters/redis_pool.rb new file mode 100644 index 000000000..2538f984f --- /dev/null +++ b/lib/flipper/adapters/redis_pool.rb @@ -0,0 +1,32 @@ +require 'flipper/adapters/redis' +require 'connection_pool' + +module Flipper + module Adapters + class RedisPool < Redis + def initialize(pool, key_prefix: nil) + @pool = pool + @key_prefix = key_prefix + end + + superclass.instance_methods(false).each do |method| + define_method method do |*args| + return super(*args) unless @client.nil? + + @pool.with do |client| + @client = client + super(*args).tap { @client = nil } + end + end + end + end + end +end + + +Flipper.configure do |config| + config.adapter do + client = ConnectionPool.new(size: 1) { Redis.new(url: ENV["FLIPPER_REDIS_URL"] || ENV["REDIS_URL"]) } + Flipper::Adapters::RedisPool.new(client) + end +end diff --git a/spec/flipper/adapters/redis_pool_spec.rb b/spec/flipper/adapters/redis_pool_spec.rb new file mode 100644 index 000000000..2898e381d --- /dev/null +++ b/spec/flipper/adapters/redis_pool_spec.rb @@ -0,0 +1,10 @@ +require 'flipper/adapters/redis_pool' + +RSpec.describe Flipper::Adapters::RedisPool do + subject do + pool = ConnectionPool.new(size: 1, &create_client) + described_class.new(pool, key_prefix: key_prefix) + end + + it_behaves_like "a redis adapter" +end diff --git a/spec/flipper/adapters/redis_spec.rb b/spec/flipper/adapters/redis_spec.rb index b4fb7472d..df06c22d3 100644 --- a/spec/flipper/adapters/redis_spec.rb +++ b/spec/flipper/adapters/redis_spec.rb @@ -1,55 +1,7 @@ require 'flipper/adapters/redis' RSpec.describe Flipper::Adapters::Redis do - let(:client) do - options = {} + subject { described_class.new(client, key_prefix: key_prefix) } - options[:url] = ENV['REDIS_URL'] if ENV['REDIS_URL'] - - Redis.raise_deprecations = true - Redis.new(options) - end - - subject { described_class.new(client) } - - before do - skip_on_error(Redis::CannotConnectError, 'Redis not available') do - client.flushdb - end - end - - it_should_behave_like 'a flipper adapter' - - it 'configures itself on load' do - Flipper.configuration = nil - Flipper.instance = nil - - silence { load 'flipper/adapters/redis.rb' } - - expect(Flipper.adapter.adapter).to be_a(Flipper::Adapters::Redis) - end - - describe 'with a key_prefix' do - let(:subject) { described_class.new(client, key_prefix: "lockbox:") } - let(:feature) { Flipper::Feature.new(:search, subject) } - - it_should_behave_like 'a flipper adapter' - - it 'namespaces feature-keys' do - subject.add(feature) - - expect(client.smembers("flipper_features")).to eq([]) - expect(client.exists?("search")).to eq(false) - expect(client.smembers("lockbox:flipper_features")).to eq(["search"]) - expect(client.hgetall("lockbox:search")).not_to eq(nil) - end - - it "can remove namespaced keys" do - subject.add(feature) - expect(client.smembers("lockbox:flipper_features")).to eq(["search"]) - - subject.remove(feature) - expect(client.smembers("lockbox:flipper_features")).to be_empty - end - end + it_behaves_like "a redis adapter" end diff --git a/spec/support/examples/redis_adapter.rb b/spec/support/examples/redis_adapter.rb new file mode 100644 index 000000000..8cfcdbada --- /dev/null +++ b/spec/support/examples/redis_adapter.rb @@ -0,0 +1,55 @@ +RSpec.shared_examples "a redis adapter" do + let(:create_client) do + Proc.new do + options = {} + + options[:url] = ENV['REDIS_URL'] if ENV['REDIS_URL'] + + Redis.raise_deprecations = true + Redis.new(options) + end + end + let(:client) { create_client.call } + let(:key_prefix) { nil } + + before do + skip_on_error(Redis::CannotConnectError, 'Redis not available') do + client.flushdb + end + end + + it_should_behave_like 'a flipper adapter' + + it 'configures itself on load' do + Flipper.configuration = nil + Flipper.instance = nil + + silence { load 'flipper/adapters/redis_pool.rb' } + + expect(Flipper.adapter.adapter).to be_a(described_class) + end + + describe 'with a key_prefix' do + let(:feature) { Flipper::Feature.new(:search, subject) } + let(:key_prefix) { "lockbox:" } + + it_should_behave_like 'a flipper adapter' + + it 'namespaces feature-keys' do + subject.add(feature) + + expect(client.smembers("flipper_features")).to eq([]) + expect(client.exists?("search")).to eq(false) + expect(client.smembers("lockbox:flipper_features")).to eq(["search"]) + expect(client.hgetall("lockbox:search")).not_to eq(nil) + end + + it "can remove namespaced keys" do + subject.add(feature) + expect(client.smembers("lockbox:flipper_features")).to eq(["search"]) + + subject.remove(feature) + expect(client.smembers("lockbox:flipper_features")).to be_empty + end + end +end diff --git a/test/adapters/redis_pool_test.rb b/test/adapters/redis_pool_test.rb new file mode 100644 index 000000000..d4f23c851 --- /dev/null +++ b/test/adapters/redis_pool_test.rb @@ -0,0 +1,15 @@ +require 'test_helper' +require 'flipper/adapters/redis_pool' + +class RedisPoolTest < MiniTest::Test + prepend Flipper::Test::SharedAdapterTests + + def setup + url = ENV.fetch('REDIS_URL', 'redis://localhost:6379') + pool = ConnectionPool.new(size: 1) { Redis.new(url: url) } + pool.with { |client| client.flushdb } + @adapter = Flipper::Adapters::RedisPool.new(pool) + rescue Redis::CannotConnectError + ENV['CI'] ? raise : skip('Redis not available') + end +end