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

Introduce Flipper::Adapters::FallbackToCached #860

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions lib/flipper/adapters/fallback_to_cached.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
require 'flipper/adapters/memoizable'

module Flipper
module Adapters
# Public: Adapter that wraps another adapter and caches the result of all
# adapter get calls in memory. If the primary adapter raises an error, the
# cached value will be used instead.
class FallbackToCached < Memoizable
def initialize(adapter, cache = nil)
super
@memoize = true
end

def memoize=(value)
# noop
end

# Public: The set of known features.
#
# Returns a set of features.
def features
response = @adapter.features
cache[@features_key] = response
response
rescue => e
cache[@features_key] || raise(e)
end

# Public: Gets the value for a feature from the primary adapter. If the
# primary adapter raises an error, the cached value will be returned
# instead.
#
# feature - The feature to get the value for.
#
# Returns the value for the feature.
def get(feature)
cache[key_for(feature.key)] = @adapter.get(feature)
rescue => e
cache[key_for(feature.key)] || raise(e)
end

# Public: Gets the values for multiple features from the primary adapter.
# If the primary adapter raises an error, the cached values will be
# returned instead.
#
# features - The features to get the values for.
#
# Returns a hash of feature keys to values.
def get_multi(features)
response = @adapter.get_multi(features)
cache.clear
features.each do |feature|
cache[key_for(feature.key)] = response[feature.key]
end
response
rescue => e
result = {}
features.each do |feature|
result[feature.key] = cache[key_for(feature.key)] || raise(e)
end
result
end

# Public: Gets all the values from the primary adapter. If the primary
# adapter raises an error, the cached values will be returned instead.
#
# Returns a hash of feature keys to values.
def get_all
response = @adapter.get_all
cache.clear
response.each do |key, value|
cache[key_for(key)] = value
end
cache[@features_key] = response.keys.to_set
response
rescue => e
raise e if cache[@features_key].empty?
response = {}
cache[@features_key].each do |key|
response[key] = cache[key_for(key)]
end
response
end
end
end
end
88 changes: 88 additions & 0 deletions spec/flipper/adapters/fallback_to_cached_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
require 'flipper/adapters/fallback_to_cached'

RSpec.describe Flipper::Adapters::FallbackToCached do
let(:adapter) { Flipper::Adapters::Memory.new }
let(:flipper) { Flipper.new(subject, memoize: false) }
let(:feature_a) { flipper[:malware_rule] }
let(:feature_b) { flipper[:spam_rule] }

subject { described_class.new(adapter) }

before do
feature_a.enable
feature_b.disable
end

describe "#features" do
it "uses primary adapter by default and caches value" do
expect(adapter).to receive(:features).and_call_original
expect(subject.features).to_not be_empty
end

it "falls back to cached value if primary adapter raises an error" do
subject.features
expect(adapter).to receive(:features).and_raise(StandardError)
expect(subject.features).to_not be_empty
end

it "raises an error if primary adapter fails and cache is empty" do
expect(adapter).to receive(:features).and_raise(StandardError)
expect { subject.features }.to raise_error StandardError
end
end

describe "#get" do
it "uses primary adapter by default and caches value" do
expect(adapter).to receive(:get).with(feature_a).and_call_original
expect(subject.get(feature_a)).to_not be_nil
end

it "falls back to cached value if primary adapter raises an error" do
subject.get(feature_a)
expect(adapter).to receive(:get).with(feature_a).and_raise(StandardError)
expect(subject.get(feature_a)).to_not be_nil
end

it "raises an error if primary adapter fails and cache is empty" do
expect(adapter).to receive(:get).with(feature_a).and_raise(StandardError)
expect { subject.get(feature_a) }.to raise_error StandardError
end
end

describe "#get_multi" do
it "uses primary adapter by default and caches value" do
expect(adapter).to receive(:get_multi).with([feature_a, feature_b]).and_call_original
expect(subject.get_multi([feature_a, feature_b])).to_not be_empty
end

it "falls back to cached value if primary adapter raises an error" do
subject.get_multi([feature_a, feature_b])
expect(adapter).to receive(:get_multi).with([feature_a, feature_b]).and_raise(StandardError)
expect(subject.get_multi([feature_a, feature_b])).to_not be_empty
end

it "raises an error if primary adapter fails and cache is empty" do
expect(adapter).to receive(:get_multi).with([feature_a, feature_b]).and_raise(StandardError)
expect { subject.get_multi([feature_a, feature_b]) }.to raise_error StandardError
end
end

describe "#get_all" do
it "uses primary adapter by default and caches value" do
expect(adapter).to receive(:get_all).and_call_original
expect(subject.get_all).to_not be_empty
end

it "falls back to cached value if primary adapter raises an error" do
subject.get_all
expect(adapter).to receive(:get_all).and_raise(StandardError)
expect(subject.get_all).to_not be_empty
end

it "raises an error if primary adapter fails and cache is empty" do
subject.cache.clear
expect(adapter).to receive(:get_all).and_raise(StandardError)
expect { subject.get_all }.to raise_error StandardError
end
end
end
Loading