Skip to content

Commit 7f1a555

Browse files
committedApr 8, 2024··
Introduce Flipper::Adapters::FallbackToCached
1 parent 45eef2f commit 7f1a555

File tree

2 files changed

+177
-0
lines changed

2 files changed

+177
-0
lines changed
 
+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
require 'flipper/adapters/memoizable'
2+
3+
module Flipper
4+
module Adapters
5+
# Public: Adapter that wraps another adapter and caches the result of all
6+
# adapter get calls in memory. If the primary adapter raises an error, the
7+
# cached value will be used instead.
8+
class FallbackToCached < Memoizable
9+
def initialize(adapter, cache = nil)
10+
super
11+
@memoize = true
12+
end
13+
14+
def memoize=(value)
15+
# raise "memoize cannot be disabled on FallbackToCached adapter"
16+
end
17+
18+
# Public: The set of known features.
19+
#
20+
# Returns a set of features.
21+
def features
22+
response = @adapter.features
23+
cache[@features_key] = response
24+
response
25+
rescue => e
26+
cache[@features_key] || raise(e)
27+
end
28+
29+
# Public: Gets the value for a feature from the primary adapter. If the
30+
# primary adapter raises an error, the cached value will be returned
31+
# instead.
32+
#
33+
# feature - The feature to get the value for.
34+
#
35+
# Returns the value for the feature.
36+
def get(feature)
37+
cache[key_for(feature.key)] = @adapter.get(feature)
38+
rescue => e
39+
cache[key_for(feature.key)] || raise(e)
40+
end
41+
42+
# Public: Gets the values for multiple features from the primary adapter.
43+
# If the primary adapter raises an error, the cached values will be
44+
# returned instead.
45+
#
46+
# features - The features to get the values for.
47+
#
48+
# Returns a hash of feature keys to values.
49+
def get_multi(features)
50+
response = @adapter.get_multi(features)
51+
cache.clear
52+
features.each do |feature|
53+
cache[key_for(feature.key)] = response[feature.key]
54+
end
55+
response
56+
rescue => e
57+
result = {}
58+
features.each do |feature|
59+
result[feature.key] = cache[key_for(feature.key)] || raise(e)
60+
end
61+
result
62+
end
63+
64+
# Public: Gets all the values from the primary adapter. If the primary
65+
# adapter raises an error, the cached values will be returned instead.
66+
#
67+
# Returns a hash of feature keys to values.
68+
def get_all
69+
response = @adapter.get_all
70+
cache.clear
71+
response.each do |key, value|
72+
cache[key_for(key)] = value
73+
end
74+
cache[@features_key] = response.keys.to_set
75+
response
76+
rescue => e
77+
raise e if cache[@features_key].empty?
78+
response = {}
79+
cache[@features_key].each do |key|
80+
response[key] = cache[key_for(key)]
81+
end
82+
# Ensures that looking up other features that do not exist doesn't
83+
# result in N+1 adapter calls.
84+
response.default_proc = ->(memo, key) { memo[key] = default_config }
85+
response
86+
end
87+
end
88+
end
89+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
require 'flipper/adapters/fallback_to_cached'
2+
3+
RSpec.describe Flipper::Adapters::FallbackToCached do
4+
let(:adapter) { Flipper::Adapters::Memory.new }
5+
let(:flipper) { Flipper.new(subject, memoize: false) }
6+
let(:feature_a) { flipper[:malware_rule] }
7+
let(:feature_b) { flipper[:spam_rule] }
8+
9+
subject { described_class.new(adapter) }
10+
11+
before do
12+
feature_a.enable
13+
feature_b.disable
14+
end
15+
16+
describe "#features" do
17+
it "uses primary adapter by default and caches value" do
18+
expect(adapter).to receive(:features).and_call_original
19+
expect(subject.features).to_not be_empty
20+
end
21+
22+
it "falls back to cached value if primary adapter raises an error" do
23+
subject.features
24+
expect(adapter).to receive(:features).and_raise(StandardError)
25+
expect(subject.features).to_not be_empty
26+
end
27+
28+
it "raises an error if primary adapter fails and cache is empty" do
29+
expect(adapter).to receive(:features).and_raise(StandardError)
30+
expect { subject.features }.to raise_error StandardError
31+
end
32+
end
33+
34+
describe "#get" do
35+
it "uses primary adapter by default and caches value" do
36+
expect(adapter).to receive(:get).with(feature_a).and_call_original
37+
expect(subject.get(feature_a)).to_not be_nil
38+
end
39+
40+
it "falls back to cached value if primary adapter raises an error" do
41+
subject.get(feature_a)
42+
expect(adapter).to receive(:get).with(feature_a).and_raise(StandardError)
43+
expect(subject.get(feature_a)).to_not be_nil
44+
end
45+
46+
it "raises an error if primary adapter fails and cache is empty" do
47+
expect(adapter).to receive(:get).with(feature_a).and_raise(StandardError)
48+
expect { subject.get(feature_a) }.to raise_error StandardError
49+
end
50+
end
51+
52+
describe "#get_multi" do
53+
it "uses primary adapter by default and caches value" do
54+
expect(adapter).to receive(:get_multi).with([feature_a, feature_b]).and_call_original
55+
expect(subject.get_multi([feature_a, feature_b])).to_not be_empty
56+
end
57+
58+
it "falls back to cached value if primary adapter raises an error" do
59+
subject.get_multi([feature_a, feature_b])
60+
expect(adapter).to receive(:get_multi).with([feature_a, feature_b]).and_raise(StandardError)
61+
expect(subject.get_multi([feature_a, feature_b])).to_not be_empty
62+
end
63+
64+
it "raises an error if primary adapter fails and cache is empty" do
65+
expect(adapter).to receive(:get_multi).with([feature_a, feature_b]).and_raise(StandardError)
66+
expect { subject.get_multi([feature_a, feature_b]) }.to raise_error StandardError
67+
end
68+
end
69+
70+
describe "#get_all" do
71+
it "uses primary adapter by default and caches value" do
72+
expect(adapter).to receive(:get_all).and_call_original
73+
expect(subject.get_all).to_not be_empty
74+
end
75+
76+
it "falls back to cached value if primary adapter raises an error" do
77+
subject.get_all
78+
expect(adapter).to receive(:get_all).and_raise(StandardError)
79+
expect(subject.get_all).to_not be_empty
80+
end
81+
82+
it "raises an error if primary adapter fails and cache is empty" do
83+
subject.cache.clear
84+
expect(adapter).to receive(:get_all).and_raise(StandardError)
85+
expect { subject.get_all }.to raise_error StandardError
86+
end
87+
end
88+
end

0 commit comments

Comments
 (0)
Please sign in to comment.