flipper-redis 0.26.0 → 1.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4fe57b8039f49c839ad48193abfe3b84781d52c43d1cbefa4ed1ab101ad5f447
4
- data.tar.gz: e469d55199c97789efc4ac225ac7e66e7631ebbe939d1b5637a8f724752f84f3
3
+ metadata.gz: 1ab27a744d523e5882067b51c63b5701a72128abc56b63ea017a7e34ee72c442
4
+ data.tar.gz: b22fdc07f401183cc8ff0715250e883f21864f75d1e9b2965d73debb22145bd1
5
5
  SHA512:
6
- metadata.gz: 9507c5f73bde9786d0c34e772b8b0314c94b6352281687cf01650557ad7b8f0136d51c0635ac249ce2a95a8e427c9fd8b2841dbeb996d78518cd1f067b6b57a0
7
- data.tar.gz: 0e050b755f0745f2d9551e3cf7a2676b99fde5b15d8181e9512a79cfacba12d6f86f35301b19c70a3ac99d41c4c16d443d45881570da81f9818986b38fd51205
6
+ metadata.gz: 77bc8292777d556eaab00f3d8c6f8b048995f3f68cc9d64973a8ae3540cd0d1a280c18c9ee7a79d19fc0295799468e294ec1bafe87cc7ebe31eae28e035dd2bd
7
+ data.tar.gz: aa80897bed12f93ef24a3220a748d3fb227b45357af6bc7c1e23e5c88db6bf763b4dce6c6b05404a2810a4c341708eec960f26c339ff069c83a2d144b9c31101
@@ -6,8 +6,8 @@ require 'flipper/adapters/redis'
6
6
  client = Redis.new
7
7
 
8
8
  # Register a few groups.
9
- Flipper.register(:admins) { |thing| thing.admin? }
10
- Flipper.register(:early_access) { |thing| thing.early_access? }
9
+ Flipper.register(:admins) { |actor| actor.admin? }
10
+ Flipper.register(:early_access) { |actor| actor.early_access? }
11
11
 
12
12
  # Create a user class that has flipper_id instance method.
13
13
  User = Struct.new(:flipper_id)
@@ -8,10 +8,10 @@ end
8
8
 
9
9
  Gem::Specification.new do |gem|
10
10
  gem.authors = ['John Nunemaker']
11
- gem.email = ['nunemaker@gmail.com']
12
- gem.summary = 'Redis adapter for Flipper'
11
+ gem.email = 'support@flippercloud.io'
12
+ gem.summary = 'Redis feature flag adapter for Flipper'
13
13
  gem.license = 'MIT'
14
- gem.homepage = 'https://github.com/jnunemaker/flipper'
14
+ gem.homepage = 'https://www.flippercloud.io/docs/adapters/redis'
15
15
 
16
16
  gem.files = `git ls-files`.split("\n").select(&flipper_redis_files) + ['lib/flipper/version.rb']
17
17
  gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n").select(&flipper_redis_files)
@@ -1,24 +1,35 @@
1
1
  require 'set'
2
2
  require 'redis'
3
3
  require 'flipper'
4
+ require 'flipper/adapters/redis_shared/methods'
4
5
 
5
6
  module Flipper
6
7
  module Adapters
7
8
  class Redis
8
9
  include ::Flipper::Adapter
10
+ include ::Flipper::Adapters::RedisShared
9
11
 
10
- # Private: The key that stores the set of known features.
11
- FeaturesKey = :flipper_features
12
+ attr_reader :key_prefix
12
13
 
13
- # Public: The name of the adapter.
14
- attr_reader :name
14
+ def features_key
15
+ "#{key_prefix}flipper_features"
16
+ end
17
+
18
+ def key_for(feature_name)
19
+ "#{key_prefix}#{feature_name}"
20
+ end
15
21
 
16
22
  # Public: Initializes a Redis flipper adapter.
17
23
  #
18
- # client - The Redis client to use. Feel free to namespace it.
19
- def initialize(client)
24
+ # client - The Redis client to use.
25
+ # key_prefix - an optional prefix with which to namespace
26
+ # flipper's Redis keys
27
+ def initialize(client, key_prefix: nil)
20
28
  @client = client
21
- @name = :redis
29
+ @key_prefix = key_prefix
30
+ @sadd_returns_boolean = with_connection do |conn|
31
+ conn.class.respond_to?(:sadd_returns_boolean) && conn.class.sadd_returns_boolean
32
+ end
22
33
  end
23
34
 
24
35
  # Public: The set of known features.
@@ -29,9 +40,9 @@ module Flipper
29
40
  # Public: Adds a feature to the set of known features.
30
41
  def add(feature)
31
42
  if redis_sadd_returns_boolean?
32
- @client.sadd? FeaturesKey, feature.key
43
+ with_connection { |conn| conn.sadd? features_key, feature.key }
33
44
  else
34
- @client.sadd FeaturesKey, feature.key
45
+ with_connection { |conn| conn.sadd features_key, feature.key }
35
46
  end
36
47
  true
37
48
  end
@@ -39,17 +50,17 @@ module Flipper
39
50
  # Public: Removes a feature from the set of known features.
40
51
  def remove(feature)
41
52
  if redis_sadd_returns_boolean?
42
- @client.srem? FeaturesKey, feature.key
53
+ with_connection { |conn| conn.srem? features_key, feature.key }
43
54
  else
44
- @client.srem FeaturesKey, feature.key
55
+ with_connection { |conn| conn.srem features_key, feature.key }
45
56
  end
46
- @client.del feature.key
57
+ with_connection { |conn| conn.del key_for(feature.key) }
47
58
  true
48
59
  end
49
60
 
50
61
  # Public: Clears the gate values for a feature.
51
62
  def clear(feature)
52
- @client.del feature.key
63
+ with_connection { |conn| conn.del key_for(feature.key) }
53
64
  true
54
65
  end
55
66
 
@@ -73,19 +84,22 @@ module Flipper
73
84
  # Public: Enables a gate for a given thing.
74
85
  #
75
86
  # feature - The Flipper::Feature for the gate.
76
- # gate - The Flipper::Gate to disable.
87
+ # gate - The Flipper::Gate to enable.
77
88
  # thing - The Flipper::Type being enabled for the gate.
78
89
  #
79
90
  # Returns true.
80
91
  def enable(feature, gate, thing)
92
+ feature_key = key_for(feature.key)
81
93
  case gate.data_type
82
94
  when :boolean
83
95
  clear(feature)
84
- @client.hset feature.key, gate.key, thing.value.to_s
96
+ with_connection { |conn| conn.hset feature_key, gate.key, thing.value.to_s }
85
97
  when :integer
86
- @client.hset feature.key, gate.key, thing.value.to_s
98
+ with_connection { |conn| conn.hset feature_key, gate.key, thing.value.to_s }
87
99
  when :set
88
- @client.hset feature.key, to_field(gate, thing), 1
100
+ with_connection { |conn| conn.hset feature_key, to_field(gate, thing), 1 }
101
+ when :json
102
+ with_connection { |conn| conn.hset feature_key, gate.key, Typecast.to_json(thing.value) }
89
103
  else
90
104
  unsupported_data_type gate.data_type
91
105
  end
@@ -101,13 +115,16 @@ module Flipper
101
115
  #
102
116
  # Returns true.
103
117
  def disable(feature, gate, thing)
118
+ feature_key = key_for(feature.key)
104
119
  case gate.data_type
105
120
  when :boolean
106
- @client.del feature.key
121
+ with_connection { |conn| conn.del feature_key }
107
122
  when :integer
108
- @client.hset feature.key, gate.key, thing.value.to_s
123
+ with_connection { |conn| conn.hset feature_key, gate.key, thing.value.to_s }
109
124
  when :set
110
- @client.hdel feature.key, to_field(gate, thing)
125
+ with_connection { |conn| conn.hdel feature_key, to_field(gate, thing) }
126
+ when :json
127
+ with_connection { |conn| conn.hdel feature_key, gate.key }
111
128
  else
112
129
  unsupported_data_type gate.data_type
113
130
  end
@@ -118,7 +135,7 @@ module Flipper
118
135
  private
119
136
 
120
137
  def redis_sadd_returns_boolean?
121
- @client.class.respond_to?(:sadd_returns_boolean) && @client.class.sadd_returns_boolean
138
+ @sadd_returns_boolean
122
139
  end
123
140
 
124
141
  def read_many_features(features)
@@ -131,20 +148,26 @@ module Flipper
131
148
  end
132
149
 
133
150
  def read_feature_keys
134
- @client.smembers(FeaturesKey).to_set
151
+ with_connection { |conn| conn.smembers(features_key).to_set }
135
152
  end
136
153
 
137
154
  # Private: Gets a hash of fields => values for the given feature.
138
155
  #
139
156
  # Returns a Hash of fields => values.
140
- def doc_for(feature, pipeline: @client)
141
- pipeline.hgetall(feature.key)
157
+ def doc_for(feature, pipeline: nil)
158
+ if pipeline
159
+ pipeline.hgetall(key_for(feature.key))
160
+ else
161
+ with_connection { |conn| conn.hgetall(key_for(feature.key)) }
162
+ end
142
163
  end
143
164
 
144
165
  def docs_for(features)
145
- @client.pipelined do |pipeline|
146
- features.each do |feature|
147
- doc_for(feature, pipeline: pipeline)
166
+ with_connection do |conn|
167
+ conn.pipelined do |pipeline|
168
+ features.each do |feature|
169
+ doc_for(feature, pipeline: pipeline)
170
+ end
148
171
  end
149
172
  end
150
173
  end
@@ -160,6 +183,9 @@ module Flipper
160
183
  doc[gate.key.to_s]
161
184
  when :set
162
185
  fields_to_gate_value fields, gate
186
+ when :json
187
+ value = doc[gate.key.to_s]
188
+ Typecast.from_json(value)
163
189
  else
164
190
  unsupported_data_type gate.data_type
165
191
  end
@@ -193,7 +219,7 @@ end
193
219
 
194
220
  Flipper.configure do |config|
195
221
  config.adapter do
196
- client = Redis.new(url: ENV["FLIPPER_REDIS_URL"] || ENV["REDIS_URL"])
222
+ client = Redis.new(url: ENV["FLIPPER_REDIS_URL"] || ENV["REDIS_URL"] || "redis://localhost:6379")
197
223
  Flipper::Adapters::Redis.new(client)
198
224
  end
199
225
  end
@@ -1,156 +1,49 @@
1
1
  require 'redis'
2
2
  require 'flipper'
3
+ require 'flipper/adapters/cache_base'
4
+ require 'flipper/adapters/redis_shared/methods'
3
5
 
4
6
  module Flipper
5
7
  module Adapters
6
8
  # Public: Adapter that wraps another adapter with the ability to cache
7
- # adapter calls in Redis
8
- class RedisCache
9
- include ::Flipper::Adapter
9
+ # adapter calls in Redis.
10
+ class RedisCache < CacheBase
11
+ include ::Flipper::Adapters::RedisShared
10
12
 
11
- Version = 'v1'.freeze
12
- Namespace = "flipper/#{Version}".freeze
13
- FeaturesKey = "#{Namespace}/features".freeze
14
- GetAllKey = "#{Namespace}/get_all".freeze
15
-
16
- # Private
17
- def self.key_for(key)
18
- "#{Namespace}/feature/#{key}"
19
- end
20
-
21
- # Internal
22
- attr_reader :cache
23
-
24
- # Public: The name of the adapter.
25
- attr_reader :name
26
-
27
- # Public
28
- def initialize(adapter, cache, ttl = 3600)
29
- @adapter = adapter
30
- @name = :redis_cache
31
- @cache = cache
32
- @ttl = ttl
33
- end
34
-
35
- # Public
36
- def features
37
- read_feature_keys
38
- end
39
-
40
- # Public
41
- def add(feature)
42
- result = @adapter.add(feature)
43
- @cache.del(FeaturesKey)
44
- result
45
- end
46
-
47
- # Public
48
- def remove(feature)
49
- result = @adapter.remove(feature)
50
- @cache.del(FeaturesKey)
51
- @cache.del(key_for(feature.key))
52
- result
53
- end
54
-
55
- # Public
56
- def clear(feature)
57
- result = @adapter.clear(feature)
58
- @cache.del(key_for(feature.key))
59
- result
60
- end
61
-
62
- # Public
63
- def get(feature)
64
- fetch(key_for(feature.key)) do
65
- @adapter.get(feature)
66
- end
67
- end
68
-
69
- def get_multi(features)
70
- read_many_features(features)
71
- end
72
-
73
- def get_all
74
- if @cache.setnx(GetAllKey, Time.now.to_i)
75
- @cache.expire(GetAllKey, @ttl)
76
- response = @adapter.get_all
77
- response.each do |key, value|
78
- set_with_ttl key_for(key), value
79
- end
80
- set_with_ttl FeaturesKey, response.keys.to_set
81
- response
82
- else
83
- features = read_feature_keys.map { |key| Flipper::Feature.new(key, self) }
84
- read_many_features(features)
85
- end
86
- end
87
-
88
- # Public
89
- def enable(feature, gate, thing)
90
- result = @adapter.enable(feature, gate, thing)
91
- @cache.del(key_for(feature.key))
92
- result
93
- end
94
-
95
- # Public
96
- def disable(feature, gate, thing)
97
- result = @adapter.disable(feature, gate, thing)
98
- @cache.del(key_for(feature.key))
99
- result
13
+ def initialize(adapter, cache, ttl = 3600, prefix: nil)
14
+ @client = cache
15
+ super
100
16
  end
101
17
 
102
18
  private
103
19
 
104
- def key_for(key)
105
- self.class.key_for(key)
106
- end
107
-
108
- def read_feature_keys
109
- fetch(FeaturesKey) { @adapter.features }
110
- end
111
-
112
- def read_many_features(features)
113
- keys = features.map(&:key)
114
- cache_result = Hash[keys.zip(multi_cache_get(keys))]
115
- uncached_features = features.reject { |feature| cache_result[feature.key] }
116
-
117
- if uncached_features.any?
118
- response = @adapter.get_multi(uncached_features)
119
- response.each do |key, value|
120
- set_with_ttl(key_for(key), value)
121
- cache_result[key] = value
122
- end
123
- end
124
-
125
- result = {}
126
- features.each do |feature|
127
- result[feature.key] = cache_result[feature.key]
128
- end
129
- result
130
- end
131
-
132
- def fetch(cache_key)
133
- cached = @cache.get(cache_key)
20
+ def cache_fetch(key, &block)
21
+ cached = with_connection { |conn| conn.get(key) }
134
22
  if cached
135
23
  Marshal.load(cached)
136
24
  else
137
25
  to_cache = yield
138
- set_with_ttl(cache_key, to_cache)
26
+ cache_write key, to_cache
139
27
  to_cache
140
28
  end
141
29
  end
142
30
 
143
- def set_with_ttl(key, value)
144
- @cache.setex(key, @ttl, Marshal.dump(value))
145
- end
146
-
147
- def multi_cache_get(keys)
148
- return [] if keys.empty?
31
+ def cache_read_multi(keys)
32
+ return {} if keys.empty?
149
33
 
150
- cache_keys = keys.map { |key| key_for(key) }
151
- @cache.mget(*cache_keys).map do |value|
34
+ values = with_connection { |conn| conn.mget(*keys) }.map do |value|
152
35
  value ? Marshal.load(value) : nil
153
36
  end
37
+
38
+ Hash[keys.zip(values)]
39
+ end
40
+
41
+ def cache_write(key, value)
42
+ with_connection { |conn| conn.setex(key, @ttl, Marshal.dump(value)) }
43
+ end
44
+
45
+ def cache_delete(key)
46
+ with_connection { |conn| conn.del(key) }
154
47
  end
155
48
  end
156
49
  end
@@ -0,0 +1,64 @@
1
+ module Flipper
2
+ module Adapters
3
+ module RedisShared
4
+ private
5
+
6
+ # Safely executes a block with a Redis connection, handling compatibility
7
+ # issues between different Redis client versions and Rails versions.
8
+ #
9
+ # This method exists to fix a compatibility issue between Rails 7.1.* and
10
+ # Redis versions below 4.7.0. The issue occurs because:
11
+ #
12
+ # 1. In Redis versions below 4.7.0, the `with` method is not defined on
13
+ # the Redis client, so Flipper would fall back to `yield(@client)`
14
+ # 2. However, Rails 7.1.* introduced `Object#with` via ActiveSupport,
15
+ # which shadows the Redis client's `with` method
16
+ # 3. Rails 7.1.*'s `Object#with` doesn't pass `self` to the block parameter
17
+ # (this was fixed in Rails 7.2.0), causing the block parameter to be `nil`
18
+ #
19
+ # This method ensures that:
20
+ # - For Redis >= 4.7.0: Uses the Redis client's native `with` method
21
+ # - For ConnectionPool: Uses the ConnectionPool's `with` method
22
+ # - For Redis < 4.7.0: Falls back to `yield(@client)` to avoid the Rails
23
+ # ActiveSupport `Object#with` method
24
+ #
25
+ # @see https://github.com/redis/redis-rb/blob/master/CHANGELOG.md#470
26
+ # @see https://github.com/rails/rails/pull/46681
27
+ # @see https://github.com/rails/rails/pull/50470
28
+ def with_connection(&block)
29
+ if client_has_correct_with_method?
30
+ @client.with(&block)
31
+ else
32
+ yield(@client)
33
+ end
34
+ end
35
+
36
+ # Determines if the Redis client has a safe `with` method that can be used
37
+ # without conflicts with Rails ActiveSupport's `Object#with`.
38
+ #
39
+ # This method checks for:
40
+ # 1. ConnectionPool instances (which have their own `with` method)
41
+ # 2. Redis instances with version >= 4.7.0 (which have a proper `with` method)
42
+ #
43
+ # The method caches its result to avoid repeated checks.
44
+ #
45
+ # @return [Boolean] true if the client has a safe `with` method, false otherwise
46
+ def client_has_correct_with_method?
47
+ return @client_has_correct_with_method if defined?(@client_has_correct_with_method)
48
+
49
+ @client_has_correct_with_method = @client.respond_to?(:with) && (client_is_connection_pool? || client_is_redis_that_has_with?)
50
+ rescue
51
+ @client_has_correct_with_method = false
52
+ end
53
+
54
+ def client_is_connection_pool?
55
+ defined?(ConnectionPool) && @client.is_a?(ConnectionPool)
56
+ end
57
+
58
+ def client_is_redis_that_has_with?
59
+ @client.is_a?(::Redis) && defined?(::Redis::VERSION) &&
60
+ ::Gem::Version.new(::Redis::VERSION) >= ::Gem::Version.new('4.7.0')
61
+ end
62
+ end
63
+ end
64
+ end
@@ -1,3 +1,13 @@
1
1
  module Flipper
2
- VERSION = '0.26.0'.freeze
2
+ VERSION = '1.3.6'.freeze
3
+
4
+ REQUIRED_RUBY_VERSION = '2.6'.freeze
5
+ NEXT_REQUIRED_RUBY_VERSION = '3.0'.freeze
6
+
7
+ REQUIRED_RAILS_VERSION = '5.2'.freeze
8
+ NEXT_REQUIRED_RAILS_VERSION = '6.1.0'.freeze
9
+
10
+ def self.deprecated_ruby_version?
11
+ Gem::Version.new(RUBY_VERSION) < Gem::Version.new(NEXT_REQUIRED_RUBY_VERSION)
12
+ end
3
13
  end
@@ -3,35 +3,98 @@ require 'flipper/adapters/redis_cache'
3
3
 
4
4
  RSpec.describe Flipper::Adapters::RedisCache do
5
5
  let(:client) do
6
- options = {}
7
- options[:url] = ENV['REDIS_URL'] if ENV['REDIS_URL']
8
- Redis.new(options)
6
+ Redis.new(url: ENV.fetch('REDIS_URL', 'redis://localhost:6379'))
9
7
  end
10
8
 
11
9
  let(:memory_adapter) do
12
10
  Flipper::Adapters::OperationLogger.new(Flipper::Adapters::Memory.new)
13
11
  end
14
- let(:adapter) { described_class.new(memory_adapter, client) }
12
+ let(:adapter) { described_class.new(memory_adapter, client, 10) }
15
13
  let(:flipper) { Flipper.new(adapter) }
16
14
 
17
15
  subject { adapter }
18
16
 
19
17
  before do
20
- begin
18
+ skip_on_error(Redis::CannotConnectError, 'Redis not available') do
21
19
  client.flushdb
22
- rescue Redis::CannotConnectError
23
- ENV['CI'] ? raise : skip('Redis not available')
24
20
  end
25
21
  end
26
22
 
27
23
  it_should_behave_like 'a flipper adapter'
28
24
 
25
+ it "knows ttl" do
26
+ expect(adapter.ttl).to eq(10)
27
+ end
28
+
29
+ it "knows features_cache_key" do
30
+ expect(adapter.features_cache_key).to eq("flipper/v1/features")
31
+ end
32
+
33
+ it "can expire features cache" do
34
+ # cache the features
35
+ adapter.features
36
+ expect(client.get("flipper/v1/features")).not_to be(nil)
37
+
38
+ # expire cache
39
+ adapter.expire_features_cache
40
+ expect(client.get("flipper/v1/features")).to be(nil)
41
+ end
42
+
43
+ it "can expire feature cache" do
44
+ # cache the features
45
+ adapter.get(flipper[:stats])
46
+ expect(client.get("flipper/v1/feature/stats")).not_to be(nil)
47
+
48
+ # expire cache
49
+ adapter.expire_feature_cache("stats")
50
+ expect(client.get("flipper/v1/feature/stats")).to be(nil)
51
+ end
52
+
53
+ it "can generate feature cache key" do
54
+ expect(adapter.feature_cache_key("stats")).to eq("flipper/v1/feature/stats")
55
+ end
56
+
57
+ context "when using a prefix" do
58
+ let(:adapter) { described_class.new(memory_adapter, client, 3600, prefix: "foo/") }
59
+ it_should_behave_like 'a flipper adapter'
60
+
61
+ it "knows features_cache_key" do
62
+ expect(adapter.features_cache_key).to eq("foo/flipper/v1/features")
63
+ end
64
+
65
+ it "can generate feature cache key" do
66
+ expect(adapter.feature_cache_key("stats")).to eq("foo/flipper/v1/feature/stats")
67
+ end
68
+
69
+ it "uses the prefix for all keys" do
70
+ # check individual feature get cached with prefix
71
+ adapter.get(flipper[:stats])
72
+ expect(Marshal.load(client.get("foo/flipper/v1/feature/stats"))).not_to be(nil)
73
+
74
+ # check individual feature expired with prefix
75
+ adapter.remove(flipper[:stats])
76
+ expect(client.get("foo/flipper/v1/feature/stats")).to be(nil)
77
+
78
+ # enable some stuff
79
+ flipper.enable_percentage_of_actors(:search, 10)
80
+ flipper.enable(:stats)
81
+
82
+ # populate the cache
83
+ adapter.get_all
84
+
85
+ # verify cached with prefix
86
+ expect(Marshal.load(client.get("foo/flipper/v1/features"))).to eq(Set["stats", "search"])
87
+ expect(Marshal.load(client.get("foo/flipper/v1/feature/search"))[:percentage_of_actors]).to eq("10")
88
+ expect(Marshal.load(client.get("foo/flipper/v1/feature/stats"))[:boolean]).to eq("true")
89
+ end
90
+ end
91
+
29
92
  describe '#remove' do
30
93
  it 'expires feature' do
31
94
  feature = flipper[:stats]
32
95
  adapter.get(feature)
33
96
  adapter.remove(feature)
34
- expect(client.get(described_class.key_for(feature))).to be(nil)
97
+ expect(client.get("flipper/v1/feature/#{feature.key}")).to be(nil)
35
98
  end
36
99
  end
37
100
 
@@ -39,7 +102,7 @@ RSpec.describe Flipper::Adapters::RedisCache do
39
102
  it 'uses correct cache key' do
40
103
  stats = flipper[:stats]
41
104
  adapter.get(stats)
42
- expect(client.get(described_class.key_for(stats))).not_to be_nil
105
+ expect(client.get("flipper/v1/feature/#{stats.key}")).not_to be_nil
43
106
  end
44
107
  end
45
108
 
@@ -54,13 +117,13 @@ RSpec.describe Flipper::Adapters::RedisCache do
54
117
  memory_adapter.reset
55
118
 
56
119
  adapter.get(stats)
57
- expect(client.get(described_class.key_for(search))).to be(nil)
58
- expect(client.get(described_class.key_for(other))).to be(nil)
120
+ expect(client.get("flipper/v1/feature/#{search.key}")).to be(nil)
121
+ expect(client.get("flipper/v1/feature/#{other.key}")).to be(nil)
59
122
 
60
123
  adapter.get_multi([stats, search, other])
61
124
 
62
125
  search_cache_value, other_cache_value = [search, other].map do |f|
63
- Marshal.load(client.get(described_class.key_for(f)))
126
+ Marshal.load(client.get("flipper/v1/feature/#{f.key}"))
64
127
  end
65
128
  expect(search_cache_value[:boolean]).to eq('true')
66
129
  expect(other_cache_value[:boolean]).to be(nil)
@@ -82,19 +145,21 @@ RSpec.describe Flipper::Adapters::RedisCache do
82
145
 
83
146
  it 'warms all features' do
84
147
  adapter.get_all
85
- expect(Marshal.load(client.get(described_class.key_for(stats.key)))[:boolean]).to eq('true')
86
- expect(Marshal.load(client.get(described_class.key_for(search.key)))[:boolean]).to be(nil)
87
- expect(client.get(described_class::GetAllKey).to_i).to be_within(2).of(Time.now.to_i)
148
+ expect(Marshal.load(client.get("flipper/v1/feature/#{stats.key}"))[:boolean]).to eq('true')
149
+ expect(Marshal.load(client.get("flipper/v1/feature/#{search.key}"))[:boolean]).to be(nil)
150
+ expect(Marshal.load(client.get("flipper/v1/features"))).to eq(Set["stats", "search"])
88
151
  end
89
152
 
90
153
  it 'returns same result when already cached' do
91
154
  expect(adapter.get_all).to eq(adapter.get_all)
92
155
  end
93
156
 
94
- it 'only invokes one call to wrapped adapter' do
157
+ it 'only invokes two calls to wrapped adapter (for features set and gate data for each feature in set)' do
95
158
  memory_adapter.reset
96
159
  5.times { adapter.get_all }
97
- expect(memory_adapter.count(:get_all)).to eq(1)
160
+ expect(memory_adapter.count(:features)).to eq(1)
161
+ expect(memory_adapter.count(:get_multi)).to eq(1)
162
+ expect(memory_adapter.count).to eq(2)
98
163
  end
99
164
  end
100
165
 
@@ -1,33 +1,101 @@
1
1
  require 'flipper/adapters/redis'
2
+ require 'connection_pool'
2
3
 
3
4
  RSpec.describe Flipper::Adapters::Redis do
4
- let(:client) do
5
- options = {}
5
+ context "redis instance" do
6
+ let(:client) do
7
+ Redis.raise_deprecations = true
8
+ Redis.new(url: ENV.fetch('REDIS_URL', 'redis://localhost:6379'))
9
+ end
6
10
 
7
- options[:url] = ENV['REDIS_URL'] if ENV['REDIS_URL']
11
+ subject { described_class.new(client) }
8
12
 
9
- Redis.raise_deprecations = true
10
- Redis.new(options)
11
- end
13
+ before do
14
+ skip_on_error(Redis::CannotConnectError, 'Redis not available') do
15
+ client.flushdb
16
+ end
17
+ end
12
18
 
13
- subject { described_class.new(client) }
19
+ it_should_behave_like 'a flipper adapter'
14
20
 
15
- before do
16
- begin
17
- client.flushdb
18
- rescue Redis::CannotConnectError
19
- ENV['CI'] ? raise : skip('Redis not available')
21
+ it 'configures itself on load' do
22
+ Flipper.configuration = nil
23
+ Flipper.instance = nil
24
+
25
+ silence { load 'flipper/adapters/redis.rb' }
26
+
27
+ expect(Flipper.adapter.adapter).to be_a(Flipper::Adapters::Redis)
28
+ end
29
+
30
+ describe 'with a key_prefix' do
31
+ let(:subject) { described_class.new(client, key_prefix: "lockbox:") }
32
+ let(:feature) { Flipper::Feature.new(:search, subject) }
33
+
34
+ it_should_behave_like 'a flipper adapter'
35
+
36
+ it 'namespaces feature-keys' do
37
+ subject.add(feature)
38
+
39
+ expect(client.smembers("flipper_features")).to eq([])
40
+ expect(client.exists?("search")).to eq(false)
41
+ expect(client.smembers("lockbox:flipper_features")).to eq(["search"])
42
+ expect(client.hgetall("lockbox:search")).not_to eq(nil)
43
+ end
44
+
45
+ it "can remove namespaced keys" do
46
+ subject.add(feature)
47
+ expect(client.smembers("lockbox:flipper_features")).to eq(["search"])
48
+
49
+ subject.remove(feature)
50
+ expect(client.smembers("lockbox:flipper_features")).to be_empty
51
+ end
20
52
  end
21
53
  end
22
54
 
23
- it_should_behave_like 'a flipper adapter'
55
+ context "with a connection pool instance" do
56
+ let(:client) do
57
+ Redis.raise_deprecations = true
58
+ ConnectionPool.new(size: 1, timeout: 1) {
59
+ Redis.new(url: ENV.fetch('REDIS_URL', 'redis://localhost:6379'))
60
+ }
61
+ end
62
+
63
+ subject { described_class.new(client) }
64
+
65
+ before do
66
+ skip_on_error(Redis::CannotConnectError, 'Redis not available') do
67
+ client.with { |conn| conn.flushdb }
68
+ end
69
+ end
70
+
71
+ it_should_behave_like 'a flipper adapter'
72
+
73
+ describe 'with a key_prefix' do
74
+ let(:subject) { described_class.new(client, key_prefix: "lockbox:") }
75
+ let(:feature) { Flipper::Feature.new(:search, subject) }
76
+
77
+ it_should_behave_like 'a flipper adapter'
24
78
 
25
- it 'configures itself on load' do
26
- Flipper.configuration = nil
27
- Flipper.instance = nil
79
+ it 'namespaces feature-keys' do
80
+ subject.add(feature)
28
81
 
29
- silence { load 'flipper/adapters/redis.rb' }
82
+ client.with do |conn|
83
+ expect(conn.smembers("flipper_features")).to eq([])
84
+ expect(conn.exists?("search")).to eq(false)
85
+ expect(conn.smembers("lockbox:flipper_features")).to eq(["search"])
86
+ expect(conn.hgetall("lockbox:search")).not_to eq(nil)
87
+ end
88
+ end
30
89
 
31
- expect(Flipper.adapter.adapter).to be_a(Flipper::Adapters::Redis)
90
+ it "can remove namespaced keys" do
91
+ client.with do |conn|
92
+ subject.add(feature)
93
+ expect(conn.smembers("lockbox:flipper_features")).to eq(["search"])
94
+
95
+ subject.remove(feature)
96
+ expect(conn.smembers("lockbox:flipper_features")).to be_empty
97
+ end
98
+ end
99
+ end
32
100
  end
33
101
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flipper-redis
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.26.0
4
+ version: 1.3.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Nunemaker
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2022-12-05 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: flipper
@@ -16,14 +15,14 @@ dependencies:
16
15
  requirements:
17
16
  - - "~>"
18
17
  - !ruby/object:Gem::Version
19
- version: 0.26.0
18
+ version: 1.3.6
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - "~>"
25
24
  - !ruby/object:Gem::Version
26
- version: 0.26.0
25
+ version: 1.3.6
27
26
  - !ruby/object:Gem::Dependency
28
27
  name: redis
29
28
  requirement: !ruby/object:Gem::Requirement
@@ -44,31 +43,33 @@ dependencies:
44
43
  - - "<"
45
44
  - !ruby/object:Gem::Version
46
45
  version: '6'
47
- description:
48
- email:
49
- - nunemaker@gmail.com
46
+ email: support@flippercloud.io
50
47
  executables: []
51
48
  extensions: []
52
49
  extra_rdoc_files: []
53
50
  files:
54
51
  - examples/redis/basic.rb
55
52
  - examples/redis/internals.rb
56
- - examples/redis/namespaced.rb
57
53
  - flipper-redis.gemspec
58
54
  - lib/flipper-redis.rb
59
55
  - lib/flipper/adapters/redis.rb
60
56
  - lib/flipper/adapters/redis_cache.rb
57
+ - lib/flipper/adapters/redis_shared/methods.rb
61
58
  - lib/flipper/version.rb
62
59
  - spec/flipper/adapters/redis_cache_spec.rb
63
60
  - spec/flipper/adapters/redis_spec.rb
64
61
  - test/adapters/redis_cache_test.rb
65
62
  - test/adapters/redis_test.rb
66
- homepage: https://github.com/jnunemaker/flipper
63
+ homepage: https://www.flippercloud.io/docs/adapters/redis
67
64
  licenses:
68
65
  - MIT
69
66
  metadata:
70
- changelog_uri: https://github.com/jnunemaker/flipper/blob/main/Changelog.md
71
- post_install_message:
67
+ documentation_uri: https://www.flippercloud.io/docs
68
+ homepage_uri: https://www.flippercloud.io
69
+ source_code_uri: https://github.com/flippercloud/flipper
70
+ bug_tracker_uri: https://github.com/flippercloud/flipper/issues
71
+ changelog_uri: https://github.com/flippercloud/flipper/releases/tag/v1.3.6
72
+ funding_uri: https://github.com/sponsors/flippercloud
72
73
  rdoc_options: []
73
74
  require_paths:
74
75
  - lib
@@ -83,10 +84,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
83
84
  - !ruby/object:Gem::Version
84
85
  version: '0'
85
86
  requirements: []
86
- rubygems_version: 3.3.7
87
- signing_key:
87
+ rubygems_version: 3.6.9
88
88
  specification_version: 4
89
- summary: Redis adapter for Flipper
89
+ summary: Redis feature flag adapter for Flipper
90
90
  test_files:
91
91
  - spec/flipper/adapters/redis_cache_spec.rb
92
92
  - spec/flipper/adapters/redis_spec.rb
@@ -1,37 +0,0 @@
1
- require 'bundler/setup'
2
- require 'redis-namespace'
3
- require 'flipper/adapters/redis'
4
-
5
- options = {url: 'redis://127.0.0.1:6379'}
6
- if ENV['REDIS_URL']
7
- options[:url] = ENV['REDIS_URL']
8
- end
9
- client = Redis.new(options)
10
- namespaced_client = Redis::Namespace.new(:flipper_namespace, redis: client)
11
- adapter = Flipper::Adapters::Redis.new(namespaced_client)
12
- flipper = Flipper.new(adapter)
13
-
14
- # Register a few groups.
15
- Flipper.register(:admins) { |thing| thing.admin? }
16
- Flipper.register(:early_access) { |thing| thing.early_access? }
17
-
18
- # Create a user class that has flipper_id instance method.
19
- User = Struct.new(:flipper_id)
20
-
21
- flipper[:stats].enable
22
- flipper[:stats].enable_group :admins
23
- flipper[:stats].enable_group :early_access
24
- flipper[:stats].enable_actor User.new('25')
25
- flipper[:stats].enable_actor User.new('90')
26
- flipper[:stats].enable_actor User.new('180')
27
- flipper[:stats].enable_percentage_of_time 15
28
- flipper[:stats].enable_percentage_of_actors 45
29
-
30
- flipper[:search].enable
31
-
32
- print 'all keys: '
33
- pp client.keys
34
- # all keys: ["stats", "flipper_features", "search"]
35
- puts
36
-
37
- puts 'notice how all the keys are namespaced'