Skip to content

Commit

Permalink
Introduce Flipper::Adapters::FallbackToCached
Browse files Browse the repository at this point in the history
  • Loading branch information
jonmagic committed Apr 8, 2024
1 parent 45eef2f commit 7f1a555
Show file tree
Hide file tree
Showing 2 changed files with 177 additions and 0 deletions.
89 changes: 89 additions & 0 deletions lib/flipper/adapters/fallback_to_cached.rb
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
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

0 comments on commit 7f1a555

Please sign in to comment.