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 +4 -4
- data/examples/redis/internals.rb +2 -2
- data/flipper-redis.gemspec +3 -3
- data/lib/flipper/adapters/redis.rb +54 -28
- data/lib/flipper/adapters/redis_cache.rb +24 -131
- data/lib/flipper/adapters/redis_shared/methods.rb +64 -0
- data/lib/flipper/version.rb +11 -1
- data/spec/flipper/adapters/redis_cache_spec.rb +82 -17
- data/spec/flipper/adapters/redis_spec.rb +86 -18
- metadata +15 -15
- data/examples/redis/namespaced.rb +0 -37
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1ab27a744d523e5882067b51c63b5701a72128abc56b63ea017a7e34ee72c442
|
|
4
|
+
data.tar.gz: b22fdc07f401183cc8ff0715250e883f21864f75d1e9b2965d73debb22145bd1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 77bc8292777d556eaab00f3d8c6f8b048995f3f68cc9d64973a8ae3540cd0d1a280c18c9ee7a79d19fc0295799468e294ec1bafe87cc7ebe31eae28e035dd2bd
|
|
7
|
+
data.tar.gz: aa80897bed12f93ef24a3220a748d3fb227b45357af6bc7c1e23e5c88db6bf763b4dce6c6b05404a2810a4c341708eec960f26c339ff069c83a2d144b9c31101
|
data/examples/redis/internals.rb
CHANGED
|
@@ -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) { |
|
|
10
|
-
Flipper.register(: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)
|
data/flipper-redis.gemspec
CHANGED
|
@@ -8,10 +8,10 @@ end
|
|
|
8
8
|
|
|
9
9
|
Gem::Specification.new do |gem|
|
|
10
10
|
gem.authors = ['John Nunemaker']
|
|
11
|
-
gem.email =
|
|
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://
|
|
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
|
-
|
|
11
|
-
FeaturesKey = :flipper_features
|
|
12
|
+
attr_reader :key_prefix
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
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.
|
|
19
|
-
|
|
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
|
-
@
|
|
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
|
-
|
|
43
|
+
with_connection { |conn| conn.sadd? features_key, feature.key }
|
|
33
44
|
else
|
|
34
|
-
|
|
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
|
-
|
|
53
|
+
with_connection { |conn| conn.srem? features_key, feature.key }
|
|
43
54
|
else
|
|
44
|
-
|
|
55
|
+
with_connection { |conn| conn.srem features_key, feature.key }
|
|
45
56
|
end
|
|
46
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
96
|
+
with_connection { |conn| conn.hset feature_key, gate.key, thing.value.to_s }
|
|
85
97
|
when :integer
|
|
86
|
-
|
|
98
|
+
with_connection { |conn| conn.hset feature_key, gate.key, thing.value.to_s }
|
|
87
99
|
when :set
|
|
88
|
-
|
|
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
|
-
|
|
121
|
+
with_connection { |conn| conn.del feature_key }
|
|
107
122
|
when :integer
|
|
108
|
-
|
|
123
|
+
with_connection { |conn| conn.hset feature_key, gate.key, thing.value.to_s }
|
|
109
124
|
when :set
|
|
110
|
-
|
|
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
|
-
@
|
|
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
|
-
|
|
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:
|
|
141
|
-
pipeline
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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::
|
|
9
|
+
# adapter calls in Redis.
|
|
10
|
+
class RedisCache < CacheBase
|
|
11
|
+
include ::Flipper::Adapters::RedisShared
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
105
|
-
|
|
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
|
-
|
|
26
|
+
cache_write key, to_cache
|
|
139
27
|
to_cache
|
|
140
28
|
end
|
|
141
29
|
end
|
|
142
30
|
|
|
143
|
-
def
|
|
144
|
-
|
|
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
|
-
|
|
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
|
data/lib/flipper/version.rb
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
1
|
module Flipper
|
|
2
|
-
VERSION = '
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
58
|
-
expect(client.get(
|
|
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(
|
|
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(
|
|
86
|
-
expect(Marshal.load(client.get(
|
|
87
|
-
expect(client.get(
|
|
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
|
|
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(:
|
|
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
|
-
|
|
5
|
-
|
|
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
|
-
|
|
11
|
+
subject { described_class.new(client) }
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
before do
|
|
14
|
+
skip_on_error(Redis::CannotConnectError, 'Redis not available') do
|
|
15
|
+
client.flushdb
|
|
16
|
+
end
|
|
17
|
+
end
|
|
12
18
|
|
|
13
|
-
|
|
19
|
+
it_should_behave_like 'a flipper adapter'
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
Flipper.instance = nil
|
|
79
|
+
it 'namespaces feature-keys' do
|
|
80
|
+
subject.add(feature)
|
|
28
81
|
|
|
29
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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://
|
|
63
|
+
homepage: https://www.flippercloud.io/docs/adapters/redis
|
|
67
64
|
licenses:
|
|
68
65
|
- MIT
|
|
69
66
|
metadata:
|
|
70
|
-
|
|
71
|
-
|
|
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.
|
|
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'
|