Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f4d2c1c
Add prefix to dalli cache
jnunemaker Jul 23, 2023
13e67b0
Add prefix to active support cache
jnunemaker Jul 23, 2023
e65c3dd
Add prefix to redis cache
jnunemaker Jul 23, 2023
4afbc47
Expose adapter being cached in cache adapters
jnunemaker Jul 23, 2023
65291c8
Stop storing a get_all key
jnunemaker Jul 27, 2023
082f011
Remove some duplication in cache adapters
jnunemaker Jul 27, 2023
3953e5f
Make features_cache_key available if people need it
jnunemaker Jul 28, 2023
d29e6ea
Merge branch 'main' into dalli-prefix
jnunemaker Feb 18, 2024
1314af1
Fix cache specs
jnunemaker Feb 18, 2024
6764d71
Fix memoizer spec for cached eager loading
jnunemaker Feb 18, 2024
2baa5af
Merge branch 'main' into dalli-prefix
jnunemaker Feb 18, 2024
a57ccf7
Run all cache changes when base changes
jnunemaker Feb 18, 2024
3eef02d
Upper case constants
jnunemaker Feb 18, 2024
0bf9286
Remove duplicate method
jnunemaker Feb 18, 2024
942525d
Move methods to top like other cache adapters
jnunemaker Feb 18, 2024
396e05a
Remove duplicate include
jnunemaker Feb 18, 2024
51a1334
Add comments and alias expires in for backwards compatibility
jnunemaker Feb 18, 2024
ba86c7e
Share the read many features logic
jnunemaker Feb 18, 2024
5327362
Make as cache store initialize backwards compat
jnunemaker Feb 18, 2024
952ddb5
Default as cache store ttl to forever as it use to be
jnunemaker Feb 18, 2024
bb75400
Remove deprecated option
jnunemaker Feb 18, 2024
eadce39
Change dalli and redis cache back to prior defaults
jnunemaker Feb 18, 2024
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
7 changes: 7 additions & 0 deletions Guardfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ guard 'rspec', rspec_options do
watch(/shared_adapter_specs\.rb$/) { 'spec' }
watch('spec/helper.rb') { 'spec' }
watch('lib/flipper/adapters/http/client.rb') { 'spec/flipper/adapters/http_spec.rb' }
watch('lib/flipper/adapters/cache_base.rb') {
[
'spec/flipper/adapters/redis_cache_spec.rb',
'spec/flipper/adapters/dalli_cache_spec.rb',
'spec/flipper/adapters/active_support_cache_store_spec.rb',
]
}

# To run all specs on every change... (useful with focus and fit)
# watch(%r{.*}) { 'spec' }
Expand Down
147 changes: 40 additions & 107 deletions lib/flipper/adapters/active_support_cache_store.rb
Original file line number Diff line number Diff line change
@@ -1,146 +1,79 @@
require 'flipper'
require 'flipper/adapters/cache_base'
require 'active_support/notifications'

module Flipper
module Adapters
# Public: Adapter that wraps another adapter with the ability to cache
# adapter calls in ActiveSupport::ActiveSupportCacheStore caches.
#
class ActiveSupportCacheStore
include ::Flipper::Adapter

# Internal
attr_reader :cache

# Public
def initialize(adapter, cache, expires_in: nil, write_through: false)
@adapter = adapter
@cache = cache
@write_options = {}
@write_options[:expires_in] = expires_in if expires_in
class ActiveSupportCacheStore < CacheBase
def initialize(adapter, cache, ttl = nil, expires_in: :none_provided, write_through: false, prefix: nil)
if expires_in == :none_provided
ttl ||= nil
else
warn "DEPRECATION WARNING: The `expires_in` kwarg is deprecated for " +
"Flipper::Adapters::ActiveSupportCacheStore and will be removed " +
"in the next major version. Please pass in expires in as third " +
"argument instead."
ttl = expires_in
end
super(adapter, cache, ttl, prefix: prefix)
@write_through = write_through

@cache_version = 'v1'.freeze
@namespace = "flipper/#{@cache_version}".freeze
@features_key = "#{@namespace}/features".freeze
@get_all_key = "#{@namespace}/get_all".freeze
end

# Public
def features
read_feature_keys
end

# Public
def add(feature)
result = @adapter.add(feature)
@cache.delete(@features_key)
result
end

## Public
def remove(feature)
result = @adapter.remove(feature)
@cache.delete(@features_key)

if @write_through
@cache.write(key_for(feature.key), default_config, @write_options)
else
@cache.delete(key_for(feature.key))
end

result
end

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

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

def get_multi(features)
read_many_features(features)
end

def get_all
if @cache.write(@get_all_key, Time.now.to_i, @write_options.merge(unless_exist: true))
response = @adapter.get_all
response.each do |key, value|
@cache.write(key_for(key), value, @write_options)
end
@cache.write(@features_key, response.keys.to_set, @write_options)
response
result = @adapter.remove(feature)
expire_features_cache
cache_write feature_cache_key(feature.key), default_config
result
else
features = read_feature_keys.map { |key| Flipper::Feature.new(key, self) }
read_many_features(features)
super
end
end

## Public
def enable(feature, gate, thing)
result = @adapter.enable(feature, gate, thing)

if @write_through
@cache.write(key_for(feature.key), @adapter.get(feature), @write_options)
result = @adapter.enable(feature, gate, thing)
cache_write feature_cache_key(feature.key), @adapter.get(feature)
result
else
@cache.delete(key_for(feature.key))
super
end

result
end

## Public
def disable(feature, gate, thing)
result = @adapter.disable(feature, gate, thing)

if @write_through
@cache.write(key_for(feature.key), @adapter.get(feature), @write_options)
result = @adapter.disable(feature, gate, thing)
cache_write feature_cache_key(feature.key), @adapter.get(feature)
result
else
@cache.delete(key_for(feature.key))
super
end

result
end

private

def key_for(key)
"#{@namespace}/feature/#{key}"
def cache_fetch(key, &block)
@cache.fetch(key, write_options, &block)
end

# Internal: Returns an array of the known feature keys.
def read_feature_keys
@cache.fetch(@features_key, @write_options) { @adapter.features }
def cache_read_multi(keys)
@cache.read_multi(*keys)
end

# Internal: Given an array of features, attempts to read through cache in
# as few network calls as possible.
def read_many_features(features)
keys = features.map { |feature| key_for(feature.key) }
cache_result = @cache.read_multi(*keys)
uncached_features = features.reject { |feature| cache_result[key_for(feature)] }
def cache_write(key, value)
@cache.write(key, value, write_options)
end

if uncached_features.any?
response = @adapter.get_multi(uncached_features)
response.each do |key, value|
@cache.write(key_for(key), value, @write_options)
cache_result[key_for(key)] = value
end
end
def cache_delete(key)
@cache.delete(key)
end

result = {}
features.each do |feature|
result[feature.key] = cache_result[key_for(feature.key)]
end
result
def write_options
write_options = {}
write_options[:expires_in] = @ttl if @ttl
write_options
end
end
end
Expand Down
143 changes: 143 additions & 0 deletions lib/flipper/adapters/cache_base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
module Flipper
module Adapters
# Base class for caching adapters. Inherit from this and then override
# cache_fetch, cache_read_multi, cache_write, and cache_delete.
class CacheBase
include ::Flipper::Adapter

# Public: The adapter being cached.
attr_reader :adapter

# Public: The ActiveSupport::Cache::Store to cache with.
attr_reader :cache

# Public: The ttl for all cached data.
attr_reader :ttl

# Public: The cache key where the set of known features is cached.
attr_reader :features_cache_key

# Public: Alias expires_in to ttl for compatibility.
alias_method :expires_in, :ttl

def initialize(adapter, cache, ttl = 300, prefix: nil)
@adapter = adapter
@cache = cache
@ttl = ttl

@cache_version = 'v1'.freeze
@namespace = "flipper/#{@cache_version}"
@namespace = @namespace.prepend(prefix) if prefix
@features_cache_key = "#{@namespace}/features"
end

# Public: Expire the cache for the set of known feature names.
def expire_features_cache
cache_delete @features_cache_key
end

# Public: Expire the cache for a given feature.
def expire_feature_cache(key)
cache_delete feature_cache_key(key)
end

# Public
def features
read_feature_keys
end

# Public
def add(feature)
result = @adapter.add(feature)
expire_features_cache
result
end

# Public
def remove(feature)
result = @adapter.remove(feature)
expire_features_cache
expire_feature_cache(feature.key)
result
end

# Public
def clear(feature)
result = @adapter.clear(feature)
expire_feature_cache(feature.key)
result
end

# Public
def get(feature)
read_feature(feature)
end

# Public
def get_multi(features)
read_many_features(features)
end

# Public
def get_all
features = read_feature_keys.map { |key| Flipper::Feature.new(key, self) }
read_many_features(features)
end

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

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

# Public: Generate the cache key for a given feature.
#
# key - The String or Symbol feature key.
def feature_cache_key(key)
"#{@namespace}/feature/#{key}"
end

private

# Private: Returns the Set of known feature keys.
def read_feature_keys
cache_fetch(@features_cache_key) { @adapter.features }
end

# Private: Read through caching for a single feature.
def read_feature(feature)
cache_fetch(feature_cache_key(feature.key)) { @adapter.get(feature) }
end

# Private: Given an array of features, attempts to read through cache in
# as few network calls as possible.
def read_many_features(features)
keys = features.map { |feature| feature_cache_key(feature.key) }
cache_result = cache_read_multi(keys)
uncached_features = features.reject { |feature| cache_result[feature_cache_key(feature)] }

if uncached_features.any?
response = @adapter.get_multi(uncached_features)
response.each do |key, value|
cache_write feature_cache_key(key), value
cache_result[feature_cache_key(key)] = value
end
end

result = {}
features.each do |feature|
result[feature.key] = cache_result[feature_cache_key(feature.key)]
end
result
end
end
end
end
Loading