Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add redis cache adapter #211

Merged
merged 14 commits into from Jan 7, 2017
8 changes: 5 additions & 3 deletions docs/DockerCompose.md
Expand Up @@ -7,9 +7,11 @@ new contributor could start working on code with a minumum efforts.
## Steps:

1. Install Docker Compose https://docs.docker.com/compose/install
2. Install gems `docker-compose run --rm app bundle install`
3. Run specs `docker-compose run --rm app bundle exec rspec`
4. Optional: log in to container an using a bash for running specs
1. Build the app container `docker-compose build`
1. Install gems `docker-compose run --rm app bundle install`
1. Run specs `docker-compose run --rm app bundle exec rspec`
1. Run tests `docker-compose run --rm app bundle exec rake test`
1. Optional: log in to container an using a bash for running specs
```sh
docker-compose run --rm app bash
bundle exec rspec
Expand Down
128 changes: 128 additions & 0 deletions lib/flipper/adapters/redis_cache.rb
@@ -0,0 +1,128 @@
require 'redis'

module Flipper
module Adapters
# Public: Adapter that wraps another adapter with the ability to cache
# adapter calls in Redis
class RedisCache
include ::Flipper::Adapter

Version = "v1".freeze
Namespace = "flipper/#{Version}".freeze
FeaturesKey = "#{Namespace}/features".freeze

# Private
def self.key_for(key)
"#{Namespace}/feature/#{key}"
end

# Internal
attr_reader :cache

# Public: The name of the adapter.
attr_reader :name

# Public
def initialize(adapter, cache, ttl = 3600)
@adapter = adapter
@name = :redis_cache
@cache = cache
@ttl = ttl
end

# Public
def features
fetch(FeaturesKey) do
@adapter.features
end
end

# Public
def add(feature)
result = @adapter.add(feature)
@cache.del(FeaturesKey)
result
end

# Public
def remove(feature)
result = @adapter.remove(feature)
@cache.del(FeaturesKey)
@cache.del(key_for(feature.key))
result
end

# Public
def clear(feature)
result = @adapter.clear(feature)
@cache.del(key_for(feature.key))
result
end

# Public
def get(feature)
fetch(key_for(feature.key)) do
@adapter.get(feature)
end
end

def get_multi(features)
keys = features.map { |feature| feature.key }
result = Hash[keys.zip(multi_cache_get(keys))]
uncached_features = features.reject do |feature|
result[feature.key]
end

if uncached_features.any?
response = @adapter.get_multi(uncached_features)
response.each do |key, value|
set_with_ttl(key_for(key), value)
result[key] = value
end
end
result
end

# Public
def enable(feature, gate, thing)
result = @adapter.enable(feature, gate, thing)
@cache.del(key_for(feature.key))
result
end

# Public
def disable(feature, gate, thing)
result = @adapter.disable(feature, gate, thing)
@cache.del(key_for(feature.key))
result
end

private

def key_for(key)
self.class.key_for(key)
end

def fetch(key, &block)
if cached = @cache.get(key)
return Marshal.load(cached)
else
to_cache = block.call
set_with_ttl(key, to_cache)
to_cache
end
end

def set_with_ttl(key, value)
@cache.setex(key, @ttl, Marshal.dump(value))
end

def multi_cache_get(keys)
cache_keys = keys.map { |key| key_for(key) }
@cache.mget(cache_keys).map do |value|
value ? Marshal.load(value) : nil
end
end
end
end
end
2 changes: 1 addition & 1 deletion spec/flipper/adapters/dalli_spec.rb
Expand Up @@ -9,7 +9,7 @@
let(:adapter) { Flipper::Adapters::Dalli.new(memory_adapter, cache) }
let(:flipper) { Flipper.new(adapter) }

subject { described_class.new(adapter, cache) }
subject { adapter }

before do
cache.flush
Expand Down
65 changes: 65 additions & 0 deletions spec/flipper/adapters/redis_cache_spec.rb
@@ -0,0 +1,65 @@
require 'helper'
require 'flipper/adapters/redis_cache'
require 'flipper/spec/shared_adapter_specs'

RSpec.describe Flipper::Adapters::RedisCache do
let(:client) {
options = {}

if ENV['BOXEN_REDIS_URL']
options[:url] = ENV['BOXEN_REDIS_URL']
end

Redis.new(options)
}

let(:memory_adapter) { Flipper::Adapters::Memory.new }
let(:cache) { Redis.new({url: ENV.fetch('BOXEN_REDIS_URL', 'localhost:6379')}) }
let(:adapter) { Flipper::Adapters::RedisCache.new(memory_adapter, cache) }
let(:flipper) { Flipper.new(adapter) }

subject { adapter }

before do
client.flushdb
end

it_should_behave_like 'a flipper adapter'

describe "#remove" do
it "expires feature" do
feature = flipper[:stats]
adapter.get(feature)
adapter.remove(feature)
expect(cache.get(described_class.key_for(feature))).to be(nil)
end
end

describe "#get_multi" do
it "warms uncached features" do
stats = flipper[:stats]
search = flipper[:search]
other = flipper[:other]
stats.enable
search.enable

adapter.get(stats)
expect(cache.get(described_class.key_for(search))).to be(nil)
expect(cache.get(described_class.key_for(other))).to be(nil)

adapter.get_multi([stats, search, other])

search_cache_value, other_cache_value = [search, other].map do |f|
Marshal.load(cache.get(described_class.key_for(f)))
end
expect(search_cache_value[:boolean]).to eq("true")
expect(other_cache_value[:boolean]).to be(nil)
end
end

describe "#name" do
it "is redis_cache" do
expect(subject.name).to be(:redis_cache)
end
end
end
1 change: 1 addition & 0 deletions spec/helper.rb
Expand Up @@ -6,6 +6,7 @@

require 'rubygems'
require 'bundler'
require 'pry'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

happy to remove this if it's not used widely


Bundler.setup(:default)

Expand Down
3 changes: 2 additions & 1 deletion test/adapters/dalli_test.rb
Expand Up @@ -6,7 +6,8 @@ class DalliTest < MiniTest::Test
prepend Flipper::Test::SharedAdapterTests

def setup
@cache = Dalli::Client.new('localhost:11211')
url = ENV.fetch('BOXEN_MEMCACHED_URL', 'localhost:11211')
@cache = Dalli::Client.new(url)
@cache.flush
memory_adapter = Flipper::Adapters::Memory.new
@adapter = Flipper::Adapters::Dalli.new(memory_adapter, @cache)
Expand Down
2 changes: 1 addition & 1 deletion test/adapters/mongo_test.rb
Expand Up @@ -5,7 +5,7 @@ class MongoTest < MiniTest::Test
prepend Flipper::Test::SharedAdapterTests

def setup
host = '127.0.0.1'
host = ENV.fetch('BOXEN_MONGODB_HOST', '127.0.0.1')
port = '27017'
logger = Logger.new("/dev/null")
collection = Mongo::Client.new(["#{host}:#{port}"], server_selection_timeout: 1, database: 'testing', logger: logger)['testing']
Expand Down
18 changes: 18 additions & 0 deletions test/adapters/redis_cache_test.rb
@@ -0,0 +1,18 @@
require 'test_helper'
require 'flipper/adapters/memory'
require 'flipper/adapters/redis_cache'

class DalliTest < MiniTest::Test
prepend Flipper::Test::SharedAdapterTests

def setup
url = ENV.fetch('BOXEN_REDIS_URL', 'localhost:6379')
@cache = Redis.new({url: url}).tap { |c| c.flushdb }
memory_adapter = Flipper::Adapters::Memory.new
@adapter = Flipper::Adapters::RedisCache.new(memory_adapter, @cache)
end

def teardown
@cache.flushdb
end
end
5 changes: 3 additions & 2 deletions test/adapters/redis_test.rb
Expand Up @@ -5,7 +5,8 @@ class RedisTest < MiniTest::Test
prepend Flipper::Test::SharedAdapterTests

def setup
client = Redis.new({}).tap { |c| c.flushdb }
@adapter = Flipper::Adapters::Redis.new(client)
url = ENV.fetch('BOXEN_REDIS_URL', 'localhost:6379')
client = Redis.new({url: url}).tap { |c| c.flushdb }
@adapter = Flipper::Adapters::Redis.new(client)
end
end