-
-
Notifications
You must be signed in to change notification settings - Fork 410
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce Flipper::Adapters::FallbackToCached
- Loading branch information
Showing
2 changed files
with
177 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
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) | ||
# raise "memoize cannot be disabled on FallbackToCached adapter" | ||
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 | ||
# Ensures that looking up other features that do not exist doesn't | ||
# result in N+1 adapter calls. | ||
response.default_proc = ->(memo, key) { memo[key] = default_config } | ||
response | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |